From 64fdcf479009e221cb25d2ad13e1374677e932d0 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sun, 9 Feb 2025 23:42:07 +0200 Subject: [PATCH 01/20] Ai generate reply / nudge endpoints --- apps/web/app/api/ai/reply/nudge/route.ts | 48 +++++++++++++++++++++ apps/web/app/api/ai/reply/route.ts | 48 +++++++++++++++++++++ apps/web/app/api/ai/summarise/controller.ts | 1 - apps/web/utils/ai/reply/generate-nudge.ts | 48 +++++++++++++++++++++ apps/web/utils/ai/reply/generate-reply.ts | 48 +++++++++++++++++++++ apps/web/utils/user/get.ts | 15 +++++++ 6 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/api/ai/reply/nudge/route.ts create mode 100644 apps/web/app/api/ai/reply/route.ts create mode 100644 apps/web/utils/ai/reply/generate-nudge.ts create mode 100644 apps/web/utils/ai/reply/generate-reply.ts create mode 100644 apps/web/utils/user/get.ts diff --git a/apps/web/app/api/ai/reply/nudge/route.ts b/apps/web/app/api/ai/reply/nudge/route.ts new file mode 100644 index 0000000000..6519df0536 --- /dev/null +++ b/apps/web/app/api/ai/reply/nudge/route.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { NextResponse } from "next/server"; +import { auth } from "@/app/api/auth/[...nextauth]/auth"; +import { withError } from "@/utils/middleware"; +import { aiGenerateNudge } from "@/utils/ai/reply/generate-nudge"; +import { getAiUserByEmail } from "@/utils/user/get"; + +const messageSchema = z + .object({ + from: z.string(), + to: z.string(), + subject: z.string(), + textPlain: z.string().optional(), + textHtml: z.string().optional(), + date: z.string(), + }) + .refine((data) => data.textPlain || data.textHtml, { + message: "At least one of textPlain or textHtml is required", + }); + +const generateReplyBody = z.object({ + messages: z.array(messageSchema), +}); + +export const POST = withError(async (request: Request) => { + const session = await auth(); + if (!session?.user.email) + return NextResponse.json({ error: "Not authenticated" }); + + const user = await getAiUserByEmail({ email: session.user.email }); + + if (!user) return NextResponse.json({ error: "User not found" }); + + const json = await request.json(); + const body = generateReplyBody.parse(json); + + const stream = await aiGenerateNudge({ + messages: body.messages.map((msg) => ({ + ...msg, + date: new Date(msg.date), + // TODO: parse content from html + content: msg.textPlain || msg.textHtml || "", + })), + user, + }); + + return stream; +}); diff --git a/apps/web/app/api/ai/reply/route.ts b/apps/web/app/api/ai/reply/route.ts new file mode 100644 index 0000000000..1b121e5bd6 --- /dev/null +++ b/apps/web/app/api/ai/reply/route.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { NextResponse } from "next/server"; +import { auth } from "@/app/api/auth/[...nextauth]/auth"; +import { withError } from "@/utils/middleware"; +import { aiGenerateReply } from "@/utils/ai/reply/generate-reply"; +import { getAiUserByEmail } from "@/utils/user/get"; + +const messageSchema = z + .object({ + from: z.string(), + to: z.string(), + subject: z.string(), + textPlain: z.string().optional(), + textHtml: z.string().optional(), + date: z.string(), + }) + .refine((data) => data.textPlain || data.textHtml, { + message: "At least one of textPlain or textHtml is required", + }); + +const generateReplyBody = z.object({ + messages: z.array(messageSchema), +}); + +export const POST = withError(async (request: Request) => { + const session = await auth(); + if (!session?.user.email) + return NextResponse.json({ error: "Not authenticated" }); + + const user = await getAiUserByEmail({ email: session.user.email }); + + if (!user) return NextResponse.json({ error: "User not found" }); + + const json = await request.json(); + const body = generateReplyBody.parse(json); + + const stream = await aiGenerateReply({ + messages: body.messages.map((msg) => ({ + ...msg, + date: new Date(msg.date), + // TODO: parse content from html + content: msg.textPlain || msg.textHtml || "", + })), + user, + }); + + return stream; +}); diff --git a/apps/web/app/api/ai/summarise/controller.ts b/apps/web/app/api/ai/summarise/controller.ts index b6080a622f..0d01d9be5b 100644 --- a/apps/web/app/api/ai/summarise/controller.ts +++ b/apps/web/app/api/ai/summarise/controller.ts @@ -1,5 +1,4 @@ import { chatCompletionStream } from "@/utils/llms"; -import { Provider } from "@/utils/llms/config"; import type { UserAIFields } from "@/utils/llms/types"; import { expire } from "@/utils/redis"; import { saveSummary } from "@/utils/redis/summary"; diff --git a/apps/web/utils/ai/reply/generate-nudge.ts b/apps/web/utils/ai/reply/generate-nudge.ts new file mode 100644 index 0000000000..32008dc620 --- /dev/null +++ b/apps/web/utils/ai/reply/generate-nudge.ts @@ -0,0 +1,48 @@ +import { chatCompletionStream } from "@/utils/llms"; +import type { UserEmailWithAI } from "@/utils/llms/types"; +import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { createScopedLogger } from "@/utils/logger"; + +// const logger = createScopedLogger("generate-nudge"); + +export async function aiGenerateNudge({ + messages, + user, +}: { + messages: { + from: string; + to: string; + subject: string; + content: string; + date: Date; + }[]; + user: UserEmailWithAI; +}) { + const system = `You are an AI assistant helping to write a follow-up email to nudge someone who hasn't responded. +Write a polite and professional email that follows up on the previous conversation. +Keep it concise and friendly. Don't be pushy. +Use context from the previous emails to make it relevant. +Don't mention that you're an AI.`; + + const prompt = `Here is the context of the email thread (from oldest to newest): +${messages + .map( + (msg) => ` +${stringifyEmail(msg, 3000)} +${msg.date.toISOString()} +`, + ) + .join("\n")} + +Please write a follow-up email to nudge for a response.`; + + const response = await chatCompletionStream({ + userAi: user, + system, + prompt, + userEmail: user.email, + usageLabel: "Reply", + }); + + return response.toTextStreamResponse(); +} diff --git a/apps/web/utils/ai/reply/generate-reply.ts b/apps/web/utils/ai/reply/generate-reply.ts new file mode 100644 index 0000000000..1d2cd76849 --- /dev/null +++ b/apps/web/utils/ai/reply/generate-reply.ts @@ -0,0 +1,48 @@ +import { chatCompletionStream } from "@/utils/llms"; +import type { UserEmailWithAI } from "@/utils/llms/types"; +import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { createScopedLogger } from "@/utils/logger"; + +// const logger = createScopedLogger("generate-reply"); + +export async function aiGenerateReply({ + messages, + user, +}: { + messages: { + from: string; + to: string; + subject: string; + content: string; + date: Date; + }[]; + user: UserEmailWithAI; +}) { + const system = `You are an AI assistant helping to draft an email reply. +Write a polite and professional email that follows up on the previous conversation. +Keep it concise and friendly. Don't be pushy. +Use context from the previous emails to make it relevant. +Don't mention that you're an AI.`; + + const prompt = `Here is the context of the email thread (from oldest to newest): +${messages + .map( + (msg) => ` +${stringifyEmail(msg, 3000)} +${msg.date.toISOString()} +`, + ) + .join("\n")} + +Please write a reply to the email.`; + + const response = await chatCompletionStream({ + userAi: user, + system, + prompt, + userEmail: user.email, + usageLabel: "Reply", + }); + + return response.toTextStreamResponse(); +} diff --git a/apps/web/utils/user/get.ts b/apps/web/utils/user/get.ts new file mode 100644 index 0000000000..477c80579d --- /dev/null +++ b/apps/web/utils/user/get.ts @@ -0,0 +1,15 @@ +import prisma from "@/utils/prisma"; + +export async function getAiUserByEmail({ email }: { email: string }) { + return prisma.user.findUnique({ + where: { email }, + select: { + id: true, + email: true, + about: true, + aiProvider: true, + aiModel: true, + aiApiKey: true, + }, + }); +} From d3072f8bc34a53ea354d76e54e4debd5fa9aeea1 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:29:14 +0200 Subject: [PATCH 02/20] Move stringify email to utils --- apps/web/utils/ai/assistant/process-user-request.ts | 2 +- apps/web/utils/ai/choose-rule/ai-choose-args.ts | 2 +- apps/web/utils/ai/choose-rule/ai-choose-rule.ts | 2 +- apps/web/utils/ai/reply/check-if-needs-reply.ts | 2 +- apps/web/utils/ai/reply/generate-nudge.ts | 2 +- apps/web/utils/ai/reply/generate-reply.ts | 2 +- apps/web/utils/ai/rule/rule-fix.ts | 2 +- apps/web/utils/ai/snippets/find-snippets.ts | 2 +- apps/web/utils/cold-email/is-cold-email.ts | 2 +- apps/web/utils/{ai/choose-rule => }/stringify-email.ts | 0 10 files changed, 9 insertions(+), 9 deletions(-) rename apps/web/utils/{ai/choose-rule => }/stringify-email.ts (100%) diff --git a/apps/web/utils/ai/assistant/process-user-request.ts b/apps/web/utils/ai/assistant/process-user-request.ts index f78a8c37d5..91a61f0607 100644 --- a/apps/web/utils/ai/assistant/process-user-request.ts +++ b/apps/web/utils/ai/assistant/process-user-request.ts @@ -27,7 +27,7 @@ import { import { updateCategoryForSender } from "@/utils/categorize/senders/categorize"; import { findSenderByEmail } from "@/utils/sender"; import { getEmailForLLM } from "@/utils/ai/choose-rule/get-email-from-message"; -import { stringifyEmailSimple } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmailSimple } from "@/utils/stringify-email"; import { updatePromptFileOnRuleCreated, updatePromptFileOnRuleUpdated, diff --git a/apps/web/utils/ai/choose-rule/ai-choose-args.ts b/apps/web/utils/ai/choose-rule/ai-choose-args.ts index 85a97acad9..2009d54e1b 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-args.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-args.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import type { UserAIFields } from "@/utils/llms/types"; import type { Action, User } from "@prisma/client"; import { chatCompletionTools, withRetry } from "@/utils/llms"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import { type EmailForLLM, type RuleWithActions, diff --git a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts index 59ca554def..2274b41ea6 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import type { UserAIFields } from "@/utils/llms/types"; import { chatCompletionObject } from "@/utils/llms"; import type { User } from "@prisma/client"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import type { EmailForLLM } from "@/utils/types"; import { createScopedLogger } from "@/utils/logger"; diff --git a/apps/web/utils/ai/reply/check-if-needs-reply.ts b/apps/web/utils/ai/reply/check-if-needs-reply.ts index 22c30208d7..4507e91e0b 100644 --- a/apps/web/utils/ai/reply/check-if-needs-reply.ts +++ b/apps/web/utils/ai/reply/check-if-needs-reply.ts @@ -7,7 +7,7 @@ import type { EmailForLLM } from "@/utils/types"; import { stringifyEmailFromBody, stringifyEmailSimple, -} from "@/utils/ai/choose-rule/stringify-email"; +} from "@/utils/stringify-email"; const logger = createScopedLogger("check-if-needs-reply"); diff --git a/apps/web/utils/ai/reply/generate-nudge.ts b/apps/web/utils/ai/reply/generate-nudge.ts index 32008dc620..ccca6ab786 100644 --- a/apps/web/utils/ai/reply/generate-nudge.ts +++ b/apps/web/utils/ai/reply/generate-nudge.ts @@ -1,6 +1,6 @@ import { chatCompletionStream } from "@/utils/llms"; import type { UserEmailWithAI } from "@/utils/llms/types"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; // const logger = createScopedLogger("generate-nudge"); diff --git a/apps/web/utils/ai/reply/generate-reply.ts b/apps/web/utils/ai/reply/generate-reply.ts index 1d2cd76849..02ff30dd43 100644 --- a/apps/web/utils/ai/reply/generate-reply.ts +++ b/apps/web/utils/ai/reply/generate-reply.ts @@ -1,6 +1,6 @@ import { chatCompletionStream } from "@/utils/llms"; import type { UserEmailWithAI } from "@/utils/llms/types"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; // const logger = createScopedLogger("generate-reply"); diff --git a/apps/web/utils/ai/rule/rule-fix.ts b/apps/web/utils/ai/rule/rule-fix.ts index 55dc531f16..ceaee087d4 100644 --- a/apps/web/utils/ai/rule/rule-fix.ts +++ b/apps/web/utils/ai/rule/rule-fix.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import type { EmailForLLM } from "@/utils/types"; import { chatCompletionObject } from "@/utils/llms"; import type { UserAIFields } from "@/utils/llms/types"; diff --git a/apps/web/utils/ai/snippets/find-snippets.ts b/apps/web/utils/ai/snippets/find-snippets.ts index 91189fb3e3..a3c30da99d 100644 --- a/apps/web/utils/ai/snippets/find-snippets.ts +++ b/apps/web/utils/ai/snippets/find-snippets.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import type { EmailForLLM } from "@/utils/types"; import { chatCompletionObject } from "@/utils/llms"; import type { UserAIFields } from "@/utils/llms/types"; diff --git a/apps/web/utils/cold-email/is-cold-email.ts b/apps/web/utils/cold-email/is-cold-email.ts index 27055d3fe1..8e687a8c29 100644 --- a/apps/web/utils/cold-email/is-cold-email.ts +++ b/apps/web/utils/cold-email/is-cold-email.ts @@ -7,7 +7,7 @@ import { labelMessage } from "@/utils/gmail/label"; import { ColdEmailSetting, ColdEmailStatus, type User } from "@prisma/client"; import prisma from "@/utils/prisma"; import { DEFAULT_COLD_EMAIL_PROMPT } from "@/utils/cold-email/prompt"; -import { stringifyEmail } from "@/utils/ai/choose-rule/stringify-email"; +import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; import { hasPreviousEmailsFromSenderOrDomain } from "@/utils/gmail/message"; diff --git a/apps/web/utils/ai/choose-rule/stringify-email.ts b/apps/web/utils/stringify-email.ts similarity index 100% rename from apps/web/utils/ai/choose-rule/stringify-email.ts rename to apps/web/utils/stringify-email.ts From 37097d49ba99e8490ee6656cfcd3e3a5675e9895 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:32:25 +0200 Subject: [PATCH 03/20] refactor get-email-from-message --- apps/web/utils/ai/assistant/process-user-request.ts | 2 +- apps/web/utils/ai/choose-rule/match-rules.ts | 2 +- apps/web/utils/ai/choose-rule/run-rules.ts | 2 +- apps/web/utils/{ai/choose-rule => }/get-email-from-message.ts | 0 apps/web/utils/reply-tracker/inbound.ts | 2 +- apps/web/utils/reply-tracker/outbound.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename apps/web/utils/{ai/choose-rule => }/get-email-from-message.ts (100%) diff --git a/apps/web/utils/ai/assistant/process-user-request.ts b/apps/web/utils/ai/assistant/process-user-request.ts index 91a61f0607..d89d049a3b 100644 --- a/apps/web/utils/ai/assistant/process-user-request.ts +++ b/apps/web/utils/ai/assistant/process-user-request.ts @@ -26,7 +26,7 @@ import { } from "@/utils/rule/rule"; import { updateCategoryForSender } from "@/utils/categorize/senders/categorize"; import { findSenderByEmail } from "@/utils/sender"; -import { getEmailForLLM } from "@/utils/ai/choose-rule/get-email-from-message"; +import { getEmailForLLM } from "@/utils/get-email-from-message"; import { stringifyEmailSimple } from "@/utils/stringify-email"; import { updatePromptFileOnRuleCreated, diff --git a/apps/web/utils/ai/choose-rule/match-rules.ts b/apps/web/utils/ai/choose-rule/match-rules.ts index a60fc699fb..0c233b1ca4 100644 --- a/apps/web/utils/ai/choose-rule/match-rules.ts +++ b/apps/web/utils/ai/choose-rule/match-rules.ts @@ -16,7 +16,7 @@ import { } from "@prisma/client"; import prisma from "@/utils/prisma"; import { aiChooseRule } from "@/utils/ai/choose-rule/ai-choose-rule"; -import { getEmailForLLM } from "@/utils/ai/choose-rule/get-email-from-message"; +import { getEmailForLLM } from "@/utils/get-email-from-message"; import { isReplyInThread } from "@/utils/thread"; import type { UserAIFields } from "@/utils/llms/types"; import { createScopedLogger } from "@/utils/logger"; diff --git a/apps/web/utils/ai/choose-rule/run-rules.ts b/apps/web/utils/ai/choose-rule/run-rules.ts index 237d67adac..665dcc1d86 100644 --- a/apps/web/utils/ai/choose-rule/run-rules.ts +++ b/apps/web/utils/ai/choose-rule/run-rules.ts @@ -14,7 +14,7 @@ import type { ActionItem } from "@/utils/ai/types"; import { findMatchingRule } from "@/utils/ai/choose-rule/match-rules"; import { getActionItemsWithAiArgs } from "@/utils/ai/choose-rule/ai-choose-args"; import { executeAct } from "@/utils/ai/choose-rule/execute"; -import { getEmailForLLM } from "@/utils/ai/choose-rule/get-email-from-message"; +import { getEmailForLLM } from "@/utils/get-email-from-message"; import prisma from "@/utils/prisma"; import { createScopedLogger } from "@/utils/logger"; import type { MatchReason } from "@/utils/ai/choose-rule/types"; diff --git a/apps/web/utils/ai/choose-rule/get-email-from-message.ts b/apps/web/utils/get-email-from-message.ts similarity index 100% rename from apps/web/utils/ai/choose-rule/get-email-from-message.ts rename to apps/web/utils/get-email-from-message.ts diff --git a/apps/web/utils/reply-tracker/inbound.ts b/apps/web/utils/reply-tracker/inbound.ts index d58422c71e..31f556d428 100644 --- a/apps/web/utils/reply-tracker/inbound.ts +++ b/apps/web/utils/reply-tracker/inbound.ts @@ -11,7 +11,7 @@ import type { UserEmailWithAI } from "@/utils/llms/types"; import type { User } from "@prisma/client"; import type { ParsedMessage } from "@/utils/types"; import { internalDateToDate } from "@/utils/date"; -import { getEmailForLLM } from "@/utils/ai/choose-rule/get-email-from-message"; +import { getEmailForLLM } from "@/utils/get-email-from-message"; import { aiChooseRule } from "@/utils/ai/choose-rule/ai-choose-rule"; import { getReplyTrackingRule } from "@/utils/reply-tracker"; diff --git a/apps/web/utils/reply-tracker/outbound.ts b/apps/web/utils/reply-tracker/outbound.ts index 916b4fedee..cd6ef73d1d 100644 --- a/apps/web/utils/reply-tracker/outbound.ts +++ b/apps/web/utils/reply-tracker/outbound.ts @@ -6,7 +6,7 @@ import prisma from "@/utils/prisma"; import { getThreadMessages } from "@/utils/gmail/thread"; import { ThreadTrackerType, type User } from "@prisma/client"; import { createScopedLogger } from "@/utils/logger"; -import { getEmailForLLM } from "@/utils/ai/choose-rule/get-email-from-message"; +import { getEmailForLLM } from "@/utils/get-email-from-message"; import { labelAwaitingReply, removeNeedsReplyLabel, From aec6cd31a093969b6c35f5abf43c51ca805b037e Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:58:10 +0200 Subject: [PATCH 04/20] Use internalDate for server side tasks --- apps/web/app/(app)/cold-email-blocker/TestRules.tsx | 1 + apps/web/app/(app)/simple/SimpleList.tsx | 4 ---- apps/web/app/api/ai/categorize/validation.ts | 2 +- apps/web/app/api/google/webhook/process-history-item.ts | 3 ++- apps/web/app/api/user/stats/tinybird/load/load-emails.ts | 5 +++-- apps/web/components/email-list/EmailList.tsx | 2 +- apps/web/utils/actions/categorize-email.ts | 7 ++++++- apps/web/utils/actions/cold-email.ts | 2 +- apps/web/utils/assistant/process-assistant-email.ts | 9 ++++++--- apps/web/utils/cold-email/is-cold-email.ts | 4 ++-- apps/web/utils/get-email-from-message.ts | 2 ++ apps/web/utils/gmail/message.ts | 4 ++-- apps/web/utils/types.ts | 3 ++- 13 files changed, 29 insertions(+), 19 deletions(-) diff --git a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx index e587b18a57..fd24f1bfcc 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx @@ -168,6 +168,7 @@ function TestRulesContentRow(props: { snippet: message.snippet || null, threadId: message.threadId, messageId: message.id, + date: message.internalDate || undefined, }); }} > diff --git a/apps/web/app/(app)/simple/SimpleList.tsx b/apps/web/app/(app)/simple/SimpleList.tsx index 513f57e376..6532406ec5 100644 --- a/apps/web/app/(app)/simple/SimpleList.tsx +++ b/apps/web/app/(app)/simple/SimpleList.tsx @@ -281,10 +281,6 @@ function SimpleListRow({ )} - - {/*
- {new Date(message.headers.date).toLocaleString()} -
*/} {!expanded &&
{actionButtons}
} diff --git a/apps/web/app/api/ai/categorize/validation.ts b/apps/web/app/api/ai/categorize/validation.ts index 7f7b6ccdef..1d8762c074 100644 --- a/apps/web/app/api/ai/categorize/validation.ts +++ b/apps/web/app/api/ai/categorize/validation.ts @@ -17,6 +17,6 @@ export const categorizeBodyWithHtml = categorizeBody.extend({ textPlain: z.string().nullable(), textHtml: z.string().nullable(), snippet: z.string().nullable(), - date: z.string(), + internalDate: z.string(), }); export type CategorizeBodyWithHtml = z.infer; diff --git a/apps/web/app/api/google/webhook/process-history-item.ts b/apps/web/app/api/google/webhook/process-history-item.ts index 356faab303..6be59434aa 100644 --- a/apps/web/app/api/google/webhook/process-history-item.ts +++ b/apps/web/app/api/google/webhook/process-history-item.ts @@ -15,6 +15,7 @@ import type { ProcessHistoryOptions } from "@/app/api/google/webhook/types"; import { ColdEmailSetting } from "@prisma/client"; import { logger } from "@/app/api/google/webhook/logger"; import { isIgnoredSender } from "@/utils/filter-ignored-senders"; +import { internalDateToDate } from "@/utils/date"; export async function processHistoryItem( { @@ -138,7 +139,7 @@ export async function processHistoryItem( content, messageId, threadId, - date: message.headers.date, + date: internalDateToDate(message.internalDate), }, gmail, user, diff --git a/apps/web/app/api/user/stats/tinybird/load/load-emails.ts b/apps/web/app/api/user/stats/tinybird/load/load-emails.ts index 658cf6b51a..02a1ced232 100644 --- a/apps/web/app/api/user/stats/tinybird/load/load-emails.ts +++ b/apps/web/app/api/user/stats/tinybird/load/load-emails.ts @@ -10,6 +10,7 @@ import { findUnsubscribeLink } from "@/utils/parse/parseHtml.server"; import { env } from "@/env"; import { GmailLabel } from "@/utils/gmail/label"; import { createScopedLogger } from "@/utils/logger"; +import { internalDateToDate } from "@/utils/date"; const PAGE_SIZE = 20; // avoid setting too high because it will hit the rate limit const PAUSE_AFTER_RATE_LIMIT = 10_000; @@ -169,7 +170,7 @@ async function saveBatch( ? extractDomainFromEmail(m.headers.to) : "Missing", subject: m.headers.subject, - timestamp: +new Date(m.headers.date), + timestamp: +internalDateToDate(m.internalDate), unsubscribeLink, read: !m.labelIds?.includes(GmailLabel.UNREAD), sent: !!m.labelIds?.includes(GmailLabel.SENT), @@ -182,7 +183,7 @@ async function saveBatch( logger.error("No timestamp for email", { ownerEmail: tinybirdEmail.ownerEmail, gmailMessageId: tinybirdEmail.gmailMessageId, - date: m.headers.date, + date: m.internalDate, }); return; } diff --git a/apps/web/components/email-list/EmailList.tsx b/apps/web/components/email-list/EmailList.tsx index 96f0990707..ef698c0a4d 100644 --- a/apps/web/components/email-list/EmailList.tsx +++ b/apps/web/components/email-list/EmailList.tsx @@ -239,7 +239,7 @@ export function EmailList({ snippet: thread.snippet, threadId: message.threadId, messageId: message.id, - date: message.headers.date, + internalDate: message.internalDate || "", }); if (isActionError(result)) { diff --git a/apps/web/utils/actions/categorize-email.ts b/apps/web/utils/actions/categorize-email.ts index 3283c6b424..a0c369bb98 100644 --- a/apps/web/utils/actions/categorize-email.ts +++ b/apps/web/utils/actions/categorize-email.ts @@ -13,6 +13,7 @@ import { truncate } from "@/utils/string"; import { withActionInstrumentation } from "@/utils/actions/middleware"; import { validateUserAndAiAccess } from "@/utils/user/validate"; import { isActionError } from "@/utils/error"; +import { internalDateToDate } from "@/utils/date"; export const categorizeEmailAction = withActionInstrumentation( "categorizeEmail", @@ -39,7 +40,11 @@ export const categorizeEmailAction = withActionInstrumentation( }); const unsubscribeLink = findUnsubscribeLink(data.textHtml); - const hasPreviousEmail = await hasPreviousEmailsFromSender(gmail, data); + const hasPreviousEmail = await hasPreviousEmailsFromSender(gmail, { + from: data.from, + messageId: data.messageId, + date: internalDateToDate(data.internalDate), + }); const res = await categorize( { diff --git a/apps/web/utils/actions/cold-email.ts b/apps/web/utils/actions/cold-email.ts index 482355b802..bd946b1fc8 100644 --- a/apps/web/utils/actions/cold-email.ts +++ b/apps/web/utils/actions/cold-email.ts @@ -122,7 +122,7 @@ async function checkColdEmail( from: body.from, subject: body.subject, content, - date: body.date, + date: body.date ? new Date(body.date) : undefined, threadId: body.threadId || undefined, messageId: body.messageId, }, diff --git a/apps/web/utils/assistant/process-assistant-email.ts b/apps/web/utils/assistant/process-assistant-email.ts index 07188e8a93..856b92cd08 100644 --- a/apps/web/utils/assistant/process-assistant-email.ts +++ b/apps/web/utils/assistant/process-assistant-email.ts @@ -10,6 +10,7 @@ import { replyToEmail } from "@/utils/gmail/mail"; import { getThreadMessages } from "@/utils/gmail/thread"; import { isAssistantEmail } from "@/utils/assistant/is-assistant-email"; import { getOrCreateInboxZeroLabel, labelMessage } from "@/utils/gmail/label"; +import { internalDateToDate } from "@/utils/date"; const logger = createScopedLogger("process-assistant-email"); @@ -169,12 +170,14 @@ async function processAssistantEmailInternal({ return; } - const firstMessageToAssistantDate = new Date( - firstMessageToAssistant.headers.date, + const firstMessageToAssistantDate = internalDateToDate( + firstMessageToAssistant.internalDate, ); const messages = threadMessages - .filter((m) => new Date(m.headers.date) >= firstMessageToAssistantDate) + .filter( + (m) => internalDateToDate(m.internalDate) >= firstMessageToAssistantDate, + ) .map((m) => { const isAssistant = isAssistantEmail({ userEmail, diff --git a/apps/web/utils/cold-email/is-cold-email.ts b/apps/web/utils/cold-email/is-cold-email.ts index 8e687a8c29..203b99c88e 100644 --- a/apps/web/utils/cold-email/is-cold-email.ts +++ b/apps/web/utils/cold-email/is-cold-email.ts @@ -29,7 +29,7 @@ export async function isColdEmail({ from: string; subject: string; content: string; - date?: string; + date?: Date; threadId?: string; messageId: string | null; }; @@ -161,7 +161,7 @@ export async function runColdEmailBlocker(options: { content: string; messageId: string; threadId: string; - date: string; + date: Date; }; gmail: gmail_v1.Gmail; user: Pick & diff --git a/apps/web/utils/get-email-from-message.ts b/apps/web/utils/get-email-from-message.ts index 536edb122c..f1c268338e 100644 --- a/apps/web/utils/get-email-from-message.ts +++ b/apps/web/utils/get-email-from-message.ts @@ -1,5 +1,6 @@ import type { ParsedMessage, EmailForLLM } from "@/utils/types"; import { emailToContent, type EmailToContentOptions } from "@/utils/mail"; +import { internalDateToDate } from "@/utils/date"; export function getEmailForLLM( message: ParsedMessage, @@ -11,5 +12,6 @@ export function getEmailForLLM( cc: message.headers.cc, subject: message.headers.subject, content: emailToContent(message, contentOptions), + date: internalDateToDate(message.internalDate), }; } diff --git a/apps/web/utils/gmail/message.ts b/apps/web/utils/gmail/message.ts index 0cba2ddea1..0f503ed8dd 100644 --- a/apps/web/utils/gmail/message.ts +++ b/apps/web/utils/gmail/message.ts @@ -101,7 +101,7 @@ async function findPreviousEmailsBySender( export async function hasPreviousEmailsFromSender( gmail: gmail_v1.Gmail, - options: { from: string; date: string; messageId: string }, + options: { from: string; date: Date; messageId: string }, ) { const previousEmails = await findPreviousEmailsBySender(gmail, { sender: options.from, @@ -133,7 +133,7 @@ const PUBLIC_DOMAINS = new Set([ export async function hasPreviousEmailsFromSenderOrDomain( gmail: gmail_v1.Gmail, - options: { from: string; date: string; messageId: string }, + options: { from: string; date: Date; messageId: string }, ) { const domain = extractDomainFromEmail(options.from); if (!domain) return hasPreviousEmailsFromSender(gmail, options); diff --git a/apps/web/utils/types.ts b/apps/web/utils/types.ts index 34fb1074ac..d666b523e2 100644 --- a/apps/web/utils/types.ts +++ b/apps/web/utils/types.ts @@ -93,7 +93,7 @@ export interface ParsedMessageHeaders { to: string; cc?: string; bcc?: string; - date: string; + date: string; // the date supplied by the email. internally we rely on message.internalDate provided by the gmail api "message-id"?: string; "reply-to"?: string; "in-reply-to"?: string; @@ -107,4 +107,5 @@ export type EmailForLLM = { cc?: string; subject: string; content: string; + date?: Date; }; From d3d6e6c79c7538c2eb992adba75c918493d71162 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:46:22 +0200 Subject: [PATCH 05/20] Update AI packages --- apps/unsubscriber/package.json | 10 +- apps/web/package.json | 12 +- pnpm-lock.yaml | 412 ++++++++++++++++----------------- 3 files changed, 214 insertions(+), 220 deletions(-) diff --git a/apps/unsubscriber/package.json b/apps/unsubscriber/package.json index 7b58d8fb4b..aeceeec7ed 100644 --- a/apps/unsubscriber/package.json +++ b/apps/unsubscriber/package.json @@ -18,13 +18,13 @@ "typescript": "5.7.2" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "^1.0.6", - "@ai-sdk/anthropic": "^1.0.8", - "@ai-sdk/google": "^1.0.12", - "@ai-sdk/openai": "^1.0.11", + "@ai-sdk/amazon-bedrock": "1.1.6", + "@ai-sdk/anthropic": "1.1.6", + "@ai-sdk/google": "1.1.11", + "@ai-sdk/openai": "1.1.9", "@fastify/cors": "^10.0.1", "@t3-oss/env-core": "^0.11.1", - "ai": "^4.0.31", + "ai": "4.1.32", "dotenv": "^16.4.7", "fastify": "^5.2.0", "zod": "^3.24.1" diff --git a/apps/web/package.json b/apps/web/package.json index 88fe536d0a..6e6582c289 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,11 +13,11 @@ "postinstall": "prisma generate" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "^1.0.6", - "@ai-sdk/anthropic": "^1.0.8", - "@ai-sdk/google": "^1.0.12", - "@ai-sdk/groq": "^1.0.10", - "@ai-sdk/openai": "^1.0.11", + "@ai-sdk/amazon-bedrock": "1.1.6", + "@ai-sdk/anthropic": "1.1.6", + "@ai-sdk/google": "1.1.11", + "@ai-sdk/groq": "1.1.7", + "@ai-sdk/openai": "1.1.9", "@asteasolutions/zod-to-openapi": "^7.3.0", "@auth/core": "^0.37.4", "@auth/prisma-adapter": "^2.7.4", @@ -74,7 +74,7 @@ "@upstash/qstash": "^2.7.20", "@upstash/redis": "^1.34.3", "@vercel/analytics": "^1.4.1", - "ai": "^4.0.31", + "ai": "4.1.32", "capital-case": "^2.0.0", "cheerio": "1.0.0", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89e4455b7d..baeb5eec7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,17 +36,17 @@ importers: apps/unsubscriber: dependencies: '@ai-sdk/amazon-bedrock': - specifier: ^1.0.6 - version: 1.0.6(zod@3.24.1) + specifier: 1.1.6 + version: 1.1.6(zod@3.24.1) '@ai-sdk/anthropic': - specifier: ^1.0.8 - version: 1.0.8(zod@3.24.1) + specifier: 1.1.6 + version: 1.1.6(zod@3.24.1) '@ai-sdk/google': - specifier: ^1.0.12 - version: 1.0.12(zod@3.24.1) + specifier: 1.1.11 + version: 1.1.11(zod@3.24.1) '@ai-sdk/openai': - specifier: ^1.0.11 - version: 1.0.11(zod@3.24.1) + specifier: 1.1.9 + version: 1.1.9(zod@3.24.1) '@fastify/cors': specifier: ^10.0.1 version: 10.0.1 @@ -54,8 +54,8 @@ importers: specifier: ^0.11.1 version: 0.11.1(typescript@5.7.2)(zod@3.24.1) ai: - specifier: ^4.0.31 - version: 4.0.31(react@18.3.1)(zod@3.24.1) + specifier: 4.1.32 + version: 4.1.32(react@18.3.1)(zod@3.24.1) dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -85,20 +85,20 @@ importers: apps/web: dependencies: '@ai-sdk/amazon-bedrock': - specifier: ^1.0.6 - version: 1.0.6(zod@3.24.1) + specifier: 1.1.6 + version: 1.1.6(zod@3.24.1) '@ai-sdk/anthropic': - specifier: ^1.0.8 - version: 1.0.8(zod@3.24.1) + specifier: 1.1.6 + version: 1.1.6(zod@3.24.1) '@ai-sdk/google': - specifier: ^1.0.12 - version: 1.0.12(zod@3.24.1) + specifier: 1.1.11 + version: 1.1.11(zod@3.24.1) '@ai-sdk/groq': - specifier: ^1.0.10 - version: 1.0.10(zod@3.24.1) + specifier: 1.1.7 + version: 1.1.7(zod@3.24.1) '@ai-sdk/openai': - specifier: ^1.0.11 - version: 1.0.11(zod@3.24.1) + specifier: 1.1.9 + version: 1.1.9(zod@3.24.1) '@asteasolutions/zod-to-openapi': specifier: ^7.3.0 version: 7.3.0(zod@3.24.1) @@ -224,7 +224,7 @@ importers: version: 1.0.2 '@sanity/vision': specifier: '3' - version: 3.56.0(@babel/runtime@7.24.1)(@codemirror/lint@6.8.1)(@codemirror/theme-one-dark@6.1.2)(@lezer/common@1.2.1)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 3.56.0(@babel/runtime@7.24.1)(@codemirror/lint@6.8.1)(@codemirror/theme-one-dark@6.1.2)(@lezer/common@1.2.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sentry/nextjs': specifier: ^8.47.0 version: 8.47.0(@opentelemetry/core@1.29.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.29.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.90.3(@swc/core@1.6.5(@swc/helpers@0.5.13))(esbuild@0.21.5)) @@ -268,8 +268,8 @@ importers: specifier: ^1.4.1 version: 1.4.1(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.12)(vue@3.4.19(typescript@5.7.2)) ai: - specifier: ^4.0.31 - version: 4.0.31(react@18.3.1)(zod@3.24.1) + specifier: 4.1.32 + version: 4.1.32(react@18.3.1)(zod@3.24.1) capital-case: specifier: ^2.0.0 version: 2.0.0 @@ -350,7 +350,7 @@ importers: version: 1.9.1(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) next-sanity: specifier: '9' - version: 9.4.7(@sanity/client@6.24.1)(@sanity/icons@3.5.6(react@18.3.1))(@sanity/types@3.68.3(@types/react@18.3.12))(@sanity/ui@2.10.12(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sanity@3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.37.0))(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(svelte@4.2.12) + version: 9.4.7(@sanity/client@6.24.1)(@sanity/icons@3.5.6(react@18.3.1))(@sanity/types@3.68.3(@types/react@18.3.12))(@sanity/ui@2.10.12(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sanity@3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.38.1))(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(svelte@4.2.12) nodemailer: specifier: ^6.9.16 version: 6.9.16 @@ -410,7 +410,7 @@ importers: version: 10.1.0(react@18.3.1) sanity: specifier: ^3.68.3 - version: 3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.37.0) + version: 3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.38.1) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -519,13 +519,13 @@ importers: version: link:../../packages/tsconfig vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2)(terser@5.37.0)) + version: 5.1.4(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2)(terser@5.38.1)) vitest: specifier: 2.1.8 - version: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0) + version: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1) vitest-mock-extended: specifier: ^2.0.2 - version: 2.0.2(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)) + version: 2.0.2(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1)) packages/eslint-config: devDependencies: @@ -537,7 +537,7 @@ importers: version: 8.18.2(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2) '@vercel/style-guide': specifier: ^6.0.0 - version: 6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2)(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)) + version: 6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2)(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1)) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@9.10.0(jiti@2.4.2)) @@ -671,32 +671,32 @@ packages: '@actions/io@1.1.3': resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} - '@ai-sdk/amazon-bedrock@1.0.6': - resolution: {integrity: sha512-EdbLjy/r9W6ds5/xbkfklr5C9y3PmGh2eXqhd3xyURq0oSoB9ukoOa9jvPTb4b3jS6l4R7yXYJvTZiAkkefUeQ==} + '@ai-sdk/amazon-bedrock@1.1.6': + resolution: {integrity: sha512-h6SJWpku+i8OsSz0A4RT2g2uD+3E0SUgWHsWRIpxmPNgM1DnH6lgSby5sxqAZDY5xJyJtRFW5vB9G3GEBjHy/g==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 - '@ai-sdk/anthropic@1.0.8': - resolution: {integrity: sha512-SruTs0JOZ5ZnVV2hzeu0XDzRrT9WHcgx9P1p5vpjJFJVr9FlVaTxgxisL+8tlhZy8FX68zAhtj09rAaL4gT+jA==} + '@ai-sdk/anthropic@1.1.6': + resolution: {integrity: sha512-4TZBg2VoU/F58DmnyfPPGU9wMUTwLP15XyAFSrUqk9sSdjszwcojXw3LE7YbxifZ+RK7wT7lTkuyK1k2UdfFng==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 - '@ai-sdk/google@1.0.12': - resolution: {integrity: sha512-vZUK8X997tKmycwCa9d26PoGtIyNEILykYb6JscMoA/pfr5Nss8Ox1JtSGn+PRkehpJhclOaLNWV1JQAjp73aA==} + '@ai-sdk/google@1.1.11': + resolution: {integrity: sha512-EcK20MTA3zNJKNOo3r52Y0N960lGL6UxUimt13HFk2RJ4dXPMWl7ZhWFgjwFXwW2QwdSPKqlMHYjne3xvKTBcQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 - '@ai-sdk/groq@1.0.10': - resolution: {integrity: sha512-FU2UT0+cz2VsaI7M0JgynC/3tXLspBBPBMauV36FfkFSeHscI1CCQlzQoTngp4mqXbfD+tJFN8gEg8qYCkawzw==} + '@ai-sdk/groq@1.1.7': + resolution: {integrity: sha512-OavkZPF42QcJUltw8N/AXmRJvHBCf+I3Nx0FFywzN8xanEEtHothdMv6qDn0nwta+5itd+DEPI+/tLTIplmsMw==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 - '@ai-sdk/openai@1.0.11': - resolution: {integrity: sha512-qI9s7Slma5i5bB4yYVlFdcG3PNDwdqivPT1Dr8adDX92nSSpILjgFIooS5yys9sXjvvcfOi/WXbDvVhLSRRlvg==} + '@ai-sdk/openai@1.1.9': + resolution: {integrity: sha512-t/CpC4TLipdbgBJTMX/otzzqzCMBSPQwUOkYPGbT/jyuC86F+YO9o+LS0Ty2pGUE1kyT+B3WmJ318B16ZCg4hw==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -710,26 +710,8 @@ packages: zod: optional: true - '@ai-sdk/provider-utils@2.0.5': - resolution: {integrity: sha512-2M7vLhYN0ThGjNlzow7oO/lsL+DyMxvGMIYmVQvEYaCWhDzxH5dOp78VNjJIVwHzVLMbBDigX3rJuzAs853idw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/provider-utils@2.0.6': - resolution: {integrity: sha512-nB0rPwIBSCk0UkfdkprAxQ45ZjfKlk+Ts5zvIBQkJ5SnTCL9meg6bW65aomQrxhdvtqZML2jjaWTI8/l6AIVlQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/provider-utils@2.0.7': - resolution: {integrity: sha512-4sfPlKEALHPXLmMFcPlYksst3sWBJXmCDZpIBJisRrmwGG6Nn3mq0N1Zu/nZaGcrWZoOY+HT2Wbxla1oTElYHQ==} + '@ai-sdk/provider-utils@2.1.6': + resolution: {integrity: sha512-Pfyaj0QZS22qyVn5Iz7IXcJ8nKIKlu2MeSAdKJzTwkAks7zdLaKVB+396Rqcp1bfQnxl7vaduQVMQiXUrgK8Gw==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -741,16 +723,12 @@ packages: resolution: {integrity: sha512-YYtP6xWQyaAf5LiWLJ+ycGTOeBLWrED7LUrvc+SQIWhGaneylqbaGsyQL7VouQUeQ4JZ1qKYZuhmi3W56HADPA==} engines: {node: '>=18'} - '@ai-sdk/provider@1.0.3': - resolution: {integrity: sha512-WiuJEpHTrltOIzv3x2wx4gwksAHW0h6nK3SoDzjqCOJLu/2OJ1yASESTIX+f07ChFykHElVoP80Ol/fe9dw6tQ==} - engines: {node: '>=18'} - - '@ai-sdk/provider@1.0.4': - resolution: {integrity: sha512-lJi5zwDosvvZER3e/pB8lj1MN3o3S7zJliQq56BRr4e9V3fcRyFtwP0JRxaRS5vHYX3OJ154VezVoQNrk0eaKw==} + '@ai-sdk/provider@1.0.7': + resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==} engines: {node: '>=18'} - '@ai-sdk/react@1.0.9': - resolution: {integrity: sha512-7mtkgVCSzp8J4x3qk5Vtlk1FiZTH7vWIZvIrA6ISbFDy+7mwm45rIDIymzCiofzr3c/Wioy41H2Ki3Nth55bgg==} + '@ai-sdk/react@1.1.11': + resolution: {integrity: sha512-vfjZ7w2M+Me83HTMMrnnrmXotz39UDCMd27YQSrvt2f1YCLPloVpLhP+Y9TLZeFE/QiiRCrPYLDQm6aQJYJ9PQ==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -761,8 +739,8 @@ packages: zod: optional: true - '@ai-sdk/ui-utils@1.0.8': - resolution: {integrity: sha512-7ya/t28oMaFauHxSj4WGQCEV/iicZj9qP+O+tCakMIDq7oDCZMUNBLCQomoWs16CcYY4l0wo1S9hA4PAdFcOvA==} + '@ai-sdk/ui-utils@1.1.11': + resolution: {integrity: sha512-1SC9W4VZLcJtxHRv4Y0aX20EFeaEP6gUvVqoKLBBtMLOgtcZrv/F/HQRjGavGugiwlS3dsVza4X+E78fiwtlTA==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -1189,6 +1167,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.26.8': + resolution: {integrity: sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3': resolution: {integrity: sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==} engines: {node: '>=6.9.0'} @@ -1771,6 +1754,10 @@ packages: resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.8': + resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -1835,19 +1822,14 @@ packages: '@codemirror/view': ^6.0.0 '@lezer/common': ^1.0.0 - '@codemirror/autocomplete@6.18.3': - resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==} - peerDependencies: - '@codemirror/language': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 + '@codemirror/autocomplete@6.18.4': + resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==} '@codemirror/commands@6.6.1': resolution: {integrity: sha512-iBfKbyIoXS1FGdsKcZmnrxmbc8VcbMrSgD7AVrsnX+WyAYjmUDWvE93dt5D874qS4CCVu4O1JpbagHdXbbLiOw==} - '@codemirror/commands@6.7.1': - resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==} + '@codemirror/commands@6.8.0': + resolution: {integrity: sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==} '@codemirror/lang-javascript@6.2.2': resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==} @@ -1855,8 +1837,8 @@ packages: '@codemirror/language@6.10.2': resolution: {integrity: sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==} - '@codemirror/language@6.10.6': - resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==} + '@codemirror/language@6.10.8': + resolution: {integrity: sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==} '@codemirror/lint@6.8.1': resolution: {integrity: sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==} @@ -1870,8 +1852,8 @@ packages: '@codemirror/state@6.4.1': resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} - '@codemirror/state@6.5.0': - resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==} + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} '@codemirror/theme-one-dark@6.1.2': resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} @@ -1879,8 +1861,8 @@ packages: '@codemirror/view@6.33.0': resolution: {integrity: sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==} - '@codemirror/view@6.35.3': - resolution: {integrity: sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==} + '@codemirror/view@6.36.2': + resolution: {integrity: sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -2548,8 +2530,8 @@ packages: resolution: {integrity: sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.5': - resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/plugin-kit@0.1.0': @@ -5938,8 +5920,8 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@4.0.31: - resolution: {integrity: sha512-+ZgOBNQAnv/liKnA/By0716cOwWjQ2UGOvWPHpqJBUL1x02YirENc0sLf8CA+gysMBJflqPKzRdv7px4NVrPUg==} + ai@4.1.32: + resolution: {integrity: sha512-+5nC5TXFhkANXoBAHMGtR7uBUdMJVA52kguAC4oovKiJdBCbcHPxAAN9f/YCZvhQ2Pcn3A0hwTRuK55yBPJsxw==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -7088,8 +7070,8 @@ packages: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} - enhanced-resolve@5.17.1: - resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -7122,6 +7104,9 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -8112,6 +8097,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-in-the-middle@1.11.2: resolution: {integrity: sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==} @@ -11086,6 +11075,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.38.1: + resolution: {integrity: sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==} + engines: {node: '>=10'} + hasBin: true + text-decoder@1.1.1: resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} @@ -12060,37 +12054,37 @@ snapshots: '@actions/io@1.1.3': {} - '@ai-sdk/amazon-bedrock@1.0.6(zod@3.24.1)': + '@ai-sdk/amazon-bedrock@1.1.6(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.3 - '@ai-sdk/provider-utils': 2.0.5(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) '@aws-sdk/client-bedrock-runtime': 3.669.0 zod: 3.24.1 transitivePeerDependencies: - aws-crt - '@ai-sdk/anthropic@1.0.8(zod@3.24.1)': + '@ai-sdk/anthropic@1.1.6(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.4 - '@ai-sdk/provider-utils': 2.0.7(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) zod: 3.24.1 - '@ai-sdk/google@1.0.12(zod@3.24.1)': + '@ai-sdk/google@1.1.11(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.3 - '@ai-sdk/provider-utils': 2.0.5(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) zod: 3.24.1 - '@ai-sdk/groq@1.0.10(zod@3.24.1)': + '@ai-sdk/groq@1.1.7(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.4 - '@ai-sdk/provider-utils': 2.0.6(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) zod: 3.24.1 - '@ai-sdk/openai@1.0.11(zod@3.24.1)': + '@ai-sdk/openai@1.1.9(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.3 - '@ai-sdk/provider-utils': 2.0.5(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) zod: 3.24.1 '@ai-sdk/provider-utils@2.0.4(zod@3.24.1)': @@ -12102,27 +12096,9 @@ snapshots: optionalDependencies: zod: 3.24.1 - '@ai-sdk/provider-utils@2.0.5(zod@3.24.1)': - dependencies: - '@ai-sdk/provider': 1.0.3 - eventsource-parser: 3.0.0 - nanoid: 3.3.8 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.24.1 - - '@ai-sdk/provider-utils@2.0.6(zod@3.24.1)': + '@ai-sdk/provider-utils@2.1.6(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.4 - eventsource-parser: 3.0.0 - nanoid: 3.3.8 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.24.1 - - '@ai-sdk/provider-utils@2.0.7(zod@3.24.1)': - dependencies: - '@ai-sdk/provider': 1.0.4 + '@ai-sdk/provider': 1.0.7 eventsource-parser: 3.0.0 nanoid: 3.3.8 secure-json-parse: 2.7.0 @@ -12133,28 +12109,24 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@1.0.3': + '@ai-sdk/provider@1.0.7': dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@1.0.4': + '@ai-sdk/react@1.1.11(react@18.3.1)(zod@3.24.1)': dependencies: - json-schema: 0.4.0 - - '@ai-sdk/react@1.0.9(react@18.3.1)(zod@3.24.1)': - dependencies: - '@ai-sdk/provider-utils': 2.0.7(zod@3.24.1) - '@ai-sdk/ui-utils': 1.0.8(zod@3.24.1) + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) + '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) swr: 2.3.0(react@18.3.1) throttleit: 2.1.0 optionalDependencies: react: 18.3.1 zod: 3.24.1 - '@ai-sdk/ui-utils@1.0.8(zod@3.24.1)': + '@ai-sdk/ui-utils@1.1.11(zod@3.24.1)': dependencies: - '@ai-sdk/provider': 1.0.4 - '@ai-sdk/provider-utils': 2.0.7(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) zod-to-json-schema: 3.24.1(zod@3.24.1) optionalDependencies: zod: 3.24.1 @@ -12981,6 +12953,11 @@ snapshots: dependencies: '@babel/types': 7.26.3 + '@babel/parser@7.26.8': + dependencies: + '@babel/types': 7.26.8 + optional: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -13747,6 +13724,12 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.26.8': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + optional: true + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -13793,11 +13776,11 @@ snapshots: '@codemirror/view': 6.33.0 '@lezer/common': 1.2.1 - '@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.1)': + '@codemirror/autocomplete@6.18.4': dependencies: - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.35.3 + '@codemirror/language': 6.10.8 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.2 '@lezer/common': 1.2.1 '@codemirror/commands@6.6.1': @@ -13807,11 +13790,11 @@ snapshots: '@codemirror/view': 6.33.0 '@lezer/common': 1.2.1 - '@codemirror/commands@6.7.1': + '@codemirror/commands@6.8.0': dependencies: - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.35.3 + '@codemirror/language': 6.10.8 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.2 '@lezer/common': 1.2.1 '@codemirror/lang-javascript@6.2.2': @@ -13833,10 +13816,10 @@ snapshots: '@lezer/lr': 1.4.2 style-mod: 4.1.2 - '@codemirror/language@6.10.6': + '@codemirror/language@6.10.8': dependencies: - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.35.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.2 '@lezer/common': 1.2.1 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -13856,21 +13839,21 @@ snapshots: '@codemirror/search@6.5.8': dependencies: - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.35.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.2 crelt: 1.0.6 '@codemirror/state@6.4.1': {} - '@codemirror/state@6.5.0': + '@codemirror/state@6.5.2': dependencies: '@marijn/find-cluster-break': 1.0.2 '@codemirror/theme-one-dark@6.1.2': dependencies: - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.35.3 + '@codemirror/language': 6.10.8 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.2 '@lezer/highlight': 1.2.1 '@codemirror/view@6.33.0': @@ -13879,9 +13862,9 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@codemirror/view@6.35.3': + '@codemirror/view@6.36.2': dependencies: - '@codemirror/state': 6.5.0 + '@codemirror/state': 6.5.2 style-mod: 4.1.2 w3c-keyname: 2.2.8 @@ -14255,7 +14238,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: - '@eslint/object-schema': 2.1.5 + '@eslint/object-schema': 2.1.6 debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: @@ -14282,7 +14265,7 @@ snapshots: espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 js-yaml: 4.1.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 @@ -14293,7 +14276,7 @@ snapshots: '@eslint/js@9.10.0': {} - '@eslint/object-schema@2.1.5': {} + '@eslint/object-schema@2.1.6': {} '@eslint/plugin-kit@0.1.0': dependencies: @@ -16511,7 +16494,7 @@ snapshots: '@types/uuid': 8.3.4 uuid: 8.3.2 - '@sanity/vision@3.56.0(@babel/runtime@7.24.1)(@codemirror/lint@6.8.1)(@codemirror/theme-one-dark@6.1.2)(@lezer/common@1.2.1)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@sanity/vision@3.56.0(@babel/runtime@7.24.1)(@codemirror/lint@6.8.1)(@codemirror/theme-one-dark@6.1.2)(@lezer/common@1.2.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@codemirror/autocomplete': 6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1) '@codemirror/commands': 6.6.1 @@ -16527,7 +16510,7 @@ snapshots: '@sanity/color': 3.0.6 '@sanity/icons': 3.5.6(react@18.3.1) '@sanity/ui': 2.8.9(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) - '@uiw/react-codemirror': 4.23.0(@babel/runtime@7.24.1)(@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.33.0)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@uiw/react-codemirror': 4.23.0(@babel/runtime@7.24.1)(@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.33.0)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) is-hotkey-esm: 1.0.0 json-2-csv: 5.5.5 json5: 2.2.3 @@ -18013,7 +17996,7 @@ snapshots: '@codemirror/state': 6.4.1 '@codemirror/view': 6.33.0 - '@uiw/react-codemirror@4.23.0(@babel/runtime@7.24.1)(@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.33.0)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.23.0(@babel/runtime@7.24.1)(@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.33.0)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.1 '@codemirror/commands': 6.6.1 @@ -18021,7 +18004,7 @@ snapshots: '@codemirror/theme-one-dark': 6.1.2 '@codemirror/view': 6.33.0 '@uiw/codemirror-extensions-basic-setup': 4.23.0(@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1))(@codemirror/commands@6.6.1)(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0) - codemirror: 6.0.1(@lezer/common@1.2.1) + codemirror: 6.0.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -18051,7 +18034,7 @@ snapshots: '@vercel/stega@0.1.2': {} - '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2)(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0))': + '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2)(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1))': dependencies: '@babel/core': 7.24.7 '@babel/eslint-parser': 7.25.0(@babel/core@7.24.7)(eslint@9.10.0(jiti@2.4.2)) @@ -18071,7 +18054,7 @@ snapshots: eslint-plugin-testing-library: 6.2.2(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-unicorn: 51.0.1(eslint@9.10.0(jiti@2.4.2)) - eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)) + eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1)) prettier-plugin-packagejson: 2.5.1(prettier@3.4.2) optionalDependencies: '@next/eslint-plugin-next': 14.2.15 @@ -18085,14 +18068,14 @@ snapshots: - supports-color - vitest - '@vitejs/plugin-react@4.3.4(vite@5.4.11(@types/node@22.10.2)(terser@5.37.0))': + '@vitejs/plugin-react@4.3.4(vite@5.4.11(@types/node@22.10.2)(terser@5.38.1))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.11(@types/node@22.10.2)(terser@5.37.0) + vite: 5.4.11(@types/node@22.10.2)(terser@5.38.1) transitivePeerDependencies: - supports-color @@ -18103,13 +18086,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.8(vite@5.1.3(@types/node@22.10.2)(terser@5.37.0))': + '@vitest/mocker@2.1.8(vite@5.1.3(@types/node@22.10.2)(terser@5.38.1))': dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.1.3(@types/node@22.10.2)(terser@5.37.0) + vite: 5.1.3(@types/node@22.10.2)(terser@5.38.1) '@vitest/pretty-format@2.1.8': dependencies: @@ -18138,7 +18121,7 @@ snapshots: '@vue/compiler-core@3.4.19': dependencies: - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.8 '@vue/shared': 3.4.19 entities: 4.5.0 estree-walker: 2.0.2 @@ -18153,7 +18136,7 @@ snapshots: '@vue/compiler-sfc@3.4.19': dependencies: - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.8 '@vue/compiler-core': 3.4.19 '@vue/compiler-dom': 3.4.19 '@vue/compiler-ssr': 3.4.19 @@ -18344,15 +18327,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@4.0.31(react@18.3.1)(zod@3.24.1): + ai@4.1.32(react@18.3.1)(zod@3.24.1): dependencies: - '@ai-sdk/provider': 1.0.4 - '@ai-sdk/provider-utils': 2.0.7(zod@3.24.1) - '@ai-sdk/react': 1.0.9(react@18.3.1)(zod@3.24.1) - '@ai-sdk/ui-utils': 1.0.8(zod@3.24.1) + '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) + '@ai-sdk/react': 1.1.11(react@18.3.1)(zod@3.24.1) + '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 - zod-to-json-schema: 3.24.1(zod@3.24.1) optionalDependencies: react: 18.3.1 zod: 3.24.1 @@ -18980,17 +18962,15 @@ snapshots: periscopic: 3.1.0 optional: true - codemirror@6.0.1(@lezer/common@1.2.1): + codemirror@6.0.1: dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.1) - '@codemirror/commands': 6.7.1 - '@codemirror/language': 6.10.6 + '@codemirror/autocomplete': 6.18.4 + '@codemirror/commands': 6.8.0 + '@codemirror/language': 6.10.8 '@codemirror/lint': 6.8.1 '@codemirror/search': 6.5.8 - '@codemirror/state': 6.5.0 - '@codemirror/view': 6.35.3 - transitivePeerDependencies: - - '@lezer/common' + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.2 collapse-white-space@2.1.0: {} @@ -19555,7 +19535,7 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 - enhanced-resolve@5.17.1: + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 @@ -19642,6 +19622,8 @@ snapshots: es-module-lexer@1.5.4: {} + es-module-lexer@1.6.0: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -20116,13 +20098,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)): + eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1)): dependencies: '@typescript-eslint/utils': 7.17.0(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2) eslint: 9.10.0(jiti@2.4.2) optionalDependencies: '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2) - vitest: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0) + vitest: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1) transitivePeerDependencies: - supports-color - typescript @@ -21123,6 +21105,11 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-in-the-middle@1.11.2: dependencies: acorn: 8.14.0 @@ -22330,7 +22317,7 @@ snapshots: use-deep-compare: 1.3.0(react@18.3.1) whatwg-fetch: 3.6.20 - next-sanity@9.4.7(@sanity/client@6.24.1)(@sanity/icons@3.5.6(react@18.3.1))(@sanity/types@3.68.3(@types/react@18.3.12))(@sanity/ui@2.10.12(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sanity@3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.37.0))(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(svelte@4.2.12): + next-sanity@9.4.7(@sanity/client@6.24.1)(@sanity/icons@3.5.6(react@18.3.1))(@sanity/types@3.68.3(@types/react@18.3.12))(@sanity/ui@2.10.12(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(next@14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sanity@3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.38.1))(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(svelte@4.2.12): dependencies: '@portabletext/react': 3.2.0(react@18.3.1) '@sanity/client': 6.24.1(debug@4.4.0) @@ -22343,7 +22330,7 @@ snapshots: history: 5.3.0 next: 14.2.15(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 - sanity: 3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.37.0) + sanity: 3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.38.1) styled-components: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@remix-run/react' @@ -23863,7 +23850,7 @@ snapshots: dependencies: '@sanity/diff-match-patch': 3.1.1 - sanity@3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.37.0): + sanity@3.68.3(@emotion/is-prop-valid@1.2.2)(@types/babel__core@7.20.5)(@types/node@22.10.2)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.38.1): dependencies: '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/modifiers': 6.0.1(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -23905,7 +23892,7 @@ snapshots: '@types/speakingurl': 13.0.6 '@types/tar-stream': 3.1.3 '@types/use-sync-external-store': 0.0.6 - '@vitejs/plugin-react': 4.3.4(vite@5.4.11(@types/node@22.10.2)(terser@5.37.0)) + '@vitejs/plugin-react': 4.3.4(vite@5.4.11(@types/node@22.10.2)(terser@5.38.1)) archiver: 7.0.1 arrify: 2.0.1 async-mutex: 0.4.1 @@ -23982,7 +23969,7 @@ snapshots: use-effect-event: 1.0.2(react@18.3.1) use-hot-module-reload: 2.0.0(react@18.3.1) use-sync-external-store: 1.2.2(react@18.3.1) - vite: 5.4.11(@types/node@22.10.2)(terser@5.37.0) + vite: 5.4.11(@types/node@22.10.2)(terser@5.38.1) yargs: 17.7.2 transitivePeerDependencies: - '@emotion/is-prop-valid' @@ -24627,7 +24614,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 - terser: 5.37.0 + terser: 5.38.1 webpack: 5.90.3(@swc/core@1.6.5(@swc/helpers@0.5.13))(esbuild@0.21.5) optionalDependencies: '@swc/core': 1.6.5(@swc/helpers@0.5.13) @@ -24640,6 +24627,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + terser@5.38.1: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-decoder@1.1.1: dependencies: b4a: 1.6.6 @@ -25198,13 +25192,13 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@2.1.8(@types/node@22.10.2)(terser@5.37.0): + vite-node@2.1.8(@types/node@22.10.2)(terser@5.38.1): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.5.4 pathe: 1.1.2 - vite: 5.1.3(@types/node@22.10.2)(terser@5.37.0) + vite: 5.1.3(@types/node@22.10.2)(terser@5.38.1) transitivePeerDependencies: - '@types/node' - less @@ -25215,18 +25209,18 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.1.4(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2)(terser@5.37.0)): + vite-tsconfig-paths@5.1.4(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.2)(terser@5.38.1)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.0(typescript@5.7.2) optionalDependencies: - vite: 5.4.11(@types/node@22.10.2)(terser@5.37.0) + vite: 5.4.11(@types/node@22.10.2)(terser@5.38.1) transitivePeerDependencies: - supports-color - typescript - vite@5.1.3(@types/node@22.10.2)(terser@5.37.0): + vite@5.1.3(@types/node@22.10.2)(terser@5.38.1): dependencies: esbuild: 0.19.11 postcss: 8.4.49 @@ -25234,9 +25228,9 @@ snapshots: optionalDependencies: '@types/node': 22.10.2 fsevents: 2.3.3 - terser: 5.37.0 + terser: 5.38.1 - vite@5.4.11(@types/node@22.10.2)(terser@5.37.0): + vite@5.4.11(@types/node@22.10.2)(terser@5.38.1): dependencies: esbuild: 0.21.5 postcss: 8.4.49 @@ -25244,18 +25238,18 @@ snapshots: optionalDependencies: '@types/node': 22.10.2 fsevents: 2.3.3 - terser: 5.37.0 + terser: 5.38.1 - vitest-mock-extended@2.0.2(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)): + vitest-mock-extended@2.0.2(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1)): dependencies: ts-essentials: 10.0.3(typescript@5.7.2) typescript: 5.7.2 - vitest: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0) + vitest: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1) - vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0): + vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.38.1): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.1.3(@types/node@22.10.2)(terser@5.37.0)) + '@vitest/mocker': 2.1.8(vite@5.1.3(@types/node@22.10.2)(terser@5.38.1)) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -25271,8 +25265,8 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.1.3(@types/node@22.10.2)(terser@5.37.0) - vite-node: 2.1.8(@types/node@22.10.2)(terser@5.37.0) + vite: 5.1.3(@types/node@22.10.2)(terser@5.38.1) + vite-node: 2.1.8(@types/node@22.10.2)(terser@5.38.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.2 @@ -25340,8 +25334,8 @@ snapshots: acorn-import-assertions: 1.9.0(acorn@8.14.0) browserslist: 4.24.3 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.17.1 - es-module-lexer: 1.5.4 + enhanced-resolve: 5.18.1 + es-module-lexer: 1.6.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 From 484229cda20af4f75f646955d3b016af02f1b88f Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:45:24 +0200 Subject: [PATCH 06/20] stream nudge --- apps/web/app/api/ai/reply/nudge/route.ts | 42 ++++++++++++++++++----- apps/web/app/api/ai/reply/route.ts | 21 +++++++----- apps/web/providers/SWRProvider.tsx | 4 +++ apps/web/utils/ai/reply/generate-nudge.ts | 15 ++++++-- apps/web/utils/ai/reply/generate-reply.ts | 2 +- apps/web/utils/redis/reply.ts | 35 +++++++++++++++++++ 6 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 apps/web/utils/redis/reply.ts diff --git a/apps/web/app/api/ai/reply/nudge/route.ts b/apps/web/app/api/ai/reply/nudge/route.ts index 6519df0536..22e818c5c0 100644 --- a/apps/web/app/api/ai/reply/nudge/route.ts +++ b/apps/web/app/api/ai/reply/nudge/route.ts @@ -4,9 +4,12 @@ import { auth } from "@/app/api/auth/[...nextauth]/auth"; import { withError } from "@/utils/middleware"; import { aiGenerateNudge } from "@/utils/ai/reply/generate-nudge"; import { getAiUserByEmail } from "@/utils/user/get"; +import { emailToContent } from "@/utils/mail"; +// import { getReply, saveReply } from "@/utils/redis/reply"; const messageSchema = z .object({ + id: z.string(), from: z.string(), to: z.string(), subject: z.string(), @@ -22,6 +25,8 @@ const generateReplyBody = z.object({ messages: z.array(messageSchema), }); +export type GenerateReplyBody = z.infer; + export const POST = withError(async (request: Request) => { const session = await auth(); if (!session?.user.email) @@ -34,15 +39,34 @@ export const POST = withError(async (request: Request) => { const json = await request.json(); const body = generateReplyBody.parse(json); - const stream = await aiGenerateNudge({ - messages: body.messages.map((msg) => ({ - ...msg, - date: new Date(msg.date), - // TODO: parse content from html - content: msg.textPlain || msg.textHtml || "", - })), + const lastMessage = body.messages.at(-1); + + if (!lastMessage) return NextResponse.json({ error: "No message provided" }); + + // const reply = await getReply({ + // userId: user.id, + // messageId: lastMessage.id, + // }); + + const messages = body.messages.map((msg) => ({ + ...msg, + date: new Date(msg.date), + content: emailToContent({ + textPlain: msg.textPlain, + textHtml: msg.textHtml, + snippet: "", + }), + })); + + return aiGenerateNudge({ + messages, user, + // onFinish: async (completion) => { + // await saveReply({ + // userId: user.id, + // messageId: lastMessage.id, + // reply: completion, + // }); + // }, }); - - return stream; }); diff --git a/apps/web/app/api/ai/reply/route.ts b/apps/web/app/api/ai/reply/route.ts index 1b121e5bd6..07b08d0a9d 100644 --- a/apps/web/app/api/ai/reply/route.ts +++ b/apps/web/app/api/ai/reply/route.ts @@ -4,6 +4,7 @@ import { auth } from "@/app/api/auth/[...nextauth]/auth"; import { withError } from "@/utils/middleware"; import { aiGenerateReply } from "@/utils/ai/reply/generate-reply"; import { getAiUserByEmail } from "@/utils/user/get"; +import { emailToContent } from "@/utils/mail"; const messageSchema = z .object({ @@ -34,15 +35,17 @@ export const POST = withError(async (request: Request) => { const json = await request.json(); const body = generateReplyBody.parse(json); - const stream = await aiGenerateReply({ - messages: body.messages.map((msg) => ({ - ...msg, - date: new Date(msg.date), - // TODO: parse content from html - content: msg.textPlain || msg.textHtml || "", - })), - user, - }); + const messages = body.messages.map((msg) => ({ + ...msg, + date: new Date(msg.date), + content: emailToContent({ + textPlain: msg.textPlain, + textHtml: msg.textHtml, + snippet: "", + }), + })); + + const stream = await aiGenerateReply({ messages, user }); return stream; }); diff --git a/apps/web/providers/SWRProvider.tsx b/apps/web/providers/SWRProvider.tsx index 278874ac5e..75f87fadae 100644 --- a/apps/web/providers/SWRProvider.tsx +++ b/apps/web/providers/SWRProvider.tsx @@ -6,6 +6,10 @@ import { captureException } from "@/utils/error"; // https://swr.vercel.app/docs/error-handling#status-code-and-error-object const fetcher = async (url: string, init?: RequestInit | undefined) => { + // Super hacky: + // https://github.com/vercel/ai/issues/3214 + if (url.startsWith("/api/ai/")) return []; + const res = await fetch(url, init); if (!res.ok) { diff --git a/apps/web/utils/ai/reply/generate-nudge.ts b/apps/web/utils/ai/reply/generate-nudge.ts index ccca6ab786..bb413b0937 100644 --- a/apps/web/utils/ai/reply/generate-nudge.ts +++ b/apps/web/utils/ai/reply/generate-nudge.ts @@ -3,11 +3,12 @@ import type { UserEmailWithAI } from "@/utils/llms/types"; import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; -// const logger = createScopedLogger("generate-nudge"); +const logger = createScopedLogger("generate-nudge"); export async function aiGenerateNudge({ messages, user, + onFinish, }: { messages: { from: string; @@ -17,12 +18,14 @@ export async function aiGenerateNudge({ date: Date; }[]; user: UserEmailWithAI; + onFinish?: (completion: string) => Promise; }) { const system = `You are an AI assistant helping to write a follow-up email to nudge someone who hasn't responded. Write a polite and professional email that follows up on the previous conversation. Keep it concise and friendly. Don't be pushy. Use context from the previous emails to make it relevant. -Don't mention that you're an AI.`; +Don't mention that you're an AI. +Don't reply with a Subject. Only reply with the body of the email.`; const prompt = `Here is the context of the email thread (from oldest to newest): ${messages @@ -36,13 +39,19 @@ ${stringifyEmail(msg, 3000)} Please write a follow-up email to nudge for a response.`; + logger.trace("Input", { system, prompt }); + const response = await chatCompletionStream({ userAi: user, system, prompt, userEmail: user.email, usageLabel: "Reply", + onFinish: async (completion) => { + logger.trace("Output", { completion }); + if (onFinish) await onFinish(completion); + }, }); - return response.toTextStreamResponse(); + return response.toDataStreamResponse(); } diff --git a/apps/web/utils/ai/reply/generate-reply.ts b/apps/web/utils/ai/reply/generate-reply.ts index 02ff30dd43..798c238883 100644 --- a/apps/web/utils/ai/reply/generate-reply.ts +++ b/apps/web/utils/ai/reply/generate-reply.ts @@ -44,5 +44,5 @@ Please write a reply to the email.`; usageLabel: "Reply", }); - return response.toTextStreamResponse(); + return response.toDataStreamResponse(); } diff --git a/apps/web/utils/redis/reply.ts b/apps/web/utils/redis/reply.ts new file mode 100644 index 0000000000..2894b0f166 --- /dev/null +++ b/apps/web/utils/redis/reply.ts @@ -0,0 +1,35 @@ +import { redis } from "@/utils/redis"; + +function getReplyKey({ + userId, + messageId, +}: { + userId: string; + messageId: string; +}) { + return `reply:${userId}:${messageId}`; +} + +export async function getReply({ + userId, + messageId, +}: { + userId: string; + messageId: string; +}): Promise { + return redis.get(getReplyKey({ userId, messageId })); +} + +export async function saveReply({ + userId, + messageId, + reply, +}: { + userId: string; + messageId: string; + reply: string; +}) { + return redis.set(getReplyKey({ userId, messageId }), reply, { + ex: 60 * 60 * 24, // 1 day + }); +} From 061cad9d8445ece309f0ea266dcdefde9f44f78e Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:45:52 +0200 Subject: [PATCH 07/20] dont stream text response --- apps/web/app/api/ai/reply/nudge/route.ts | 20 +--- .../web/components/email-list/EmailThread.tsx | 93 +++++++++++++------ apps/web/utils/actions/generate-reply.ts | 78 ++++++++++++++++ .../actions/generate-reply.validation.ts | 21 +++++ apps/web/utils/ai/reply/generate-nudge.ts | 13 +-- apps/web/utils/ai/reply/generate-reply.ts | 12 ++- apps/web/utils/auth.ts | 26 +++--- apps/web/utils/llms/index.ts | 40 ++++++++ 8 files changed, 235 insertions(+), 68 deletions(-) create mode 100644 apps/web/utils/actions/generate-reply.ts create mode 100644 apps/web/utils/actions/generate-reply.validation.ts diff --git a/apps/web/app/api/ai/reply/nudge/route.ts b/apps/web/app/api/ai/reply/nudge/route.ts index 22e818c5c0..e78f33d928 100644 --- a/apps/web/app/api/ai/reply/nudge/route.ts +++ b/apps/web/app/api/ai/reply/nudge/route.ts @@ -5,7 +5,6 @@ import { withError } from "@/utils/middleware"; import { aiGenerateNudge } from "@/utils/ai/reply/generate-nudge"; import { getAiUserByEmail } from "@/utils/user/get"; import { emailToContent } from "@/utils/mail"; -// import { getReply, saveReply } from "@/utils/redis/reply"; const messageSchema = z .object({ @@ -43,11 +42,6 @@ export const POST = withError(async (request: Request) => { if (!lastMessage) return NextResponse.json({ error: "No message provided" }); - // const reply = await getReply({ - // userId: user.id, - // messageId: lastMessage.id, - // }); - const messages = body.messages.map((msg) => ({ ...msg, date: new Date(msg.date), @@ -58,15 +52,7 @@ export const POST = withError(async (request: Request) => { }), })); - return aiGenerateNudge({ - messages, - user, - // onFinish: async (completion) => { - // await saveReply({ - // userId: user.id, - // messageId: lastMessage.id, - // reply: completion, - // }); - // }, - }); + const text = await aiGenerateNudge({ messages, user }); + + return NextResponse.json({ text }); }); diff --git a/apps/web/components/email-list/EmailThread.tsx b/apps/web/components/email-list/EmailThread.tsx index 4fd33f91da..354041095a 100644 --- a/apps/web/components/email-list/EmailThread.tsx +++ b/apps/web/components/email-list/EmailThread.tsx @@ -29,6 +29,8 @@ import { extractEmailReply } from "@/utils/parse/extract-reply.client"; import type { ReplyingToEmail } from "@/app/(app)/compose/ComposeEmailForm"; import { createReplyContent } from "@/utils/gmail/reply"; import { cn } from "@/utils"; +import { useCompletion } from "ai/react"; +import type { GenerateReplyBody } from "@/app/api/ai/reply/nudge/route"; type EmailMessage = Thread["messages"][number]; @@ -87,31 +89,34 @@ export function EmailThread({ )}
    - {organizedMessages.map(({ message, draftReply }) => ( - { - setExpandedMessageIds((prev) => { - if (prev.has(message.id)) return prev; - return new Set(prev).add(message.id); - }); - }} - onSendSuccess={(messageId) => { - setExpandedMessageIds((prev) => { - if (prev.has(messageId)) return prev; - return new Set(prev).add(messageId); - }); - }} - /> - ))} + {organizedMessages.map(({ message, draftReply }) => { + const defaultShowReply = + autoOpenReplyForMessageId === message.id || Boolean(draftReply); + return ( + { + setExpandedMessageIds((prev) => { + if (prev.has(message.id)) return prev; + return new Set(prev).add(message.id); + }); + }} + onSendSuccess={(messageId) => { + setExpandedMessageIds((prev) => { + if (prev.has(messageId)) return prev; + return new Set(prev).add(messageId); + }); + }} + generateNudge={defaultShowReply && !draftReply?.textHtml} + /> + ); + })}
); @@ -126,6 +131,7 @@ function EmailMessage({ expanded, onExpand, onSendSuccess, + generateNudge, }: { message: EmailMessage; draftReply?: EmailMessage; @@ -135,6 +141,7 @@ function EmailMessage({ expanded: boolean; onExpand: () => void; onSendSuccess: (messageId: string) => void; + generateNudge?: boolean; }) { const [showReply, setShowReply] = useState(defaultShowReply || false); const replyRef = useRef(null); @@ -159,6 +166,37 @@ function EmailMessage({ setShowForward(false); }, []); + const body: GenerateReplyBody = { + messages: [ + { + id: message.id, + textHtml: message.textHtml, + textPlain: message.textPlain, + date: message.headers.date, + from: message.headers.from, + to: message.headers.to, + subject: message.headers.subject, + }, + ], + }; + + const { completion, complete, error, isLoading } = useCompletion({ + api: "/api/ai/reply/nudge", + body, + }); + console.log("🚀 ~ completion:", completion); + + if (error) { + console.error("There was an error generating the nudge", error); + } + + useEffect(() => { + if (generateNudge) { + // we send the data via the body instead + complete(""); + } + }, [complete, generateNudge]); + const replyingToEmail: ReplyingToEmail = useMemo(() => { if (showReply) { if (draftReply) return prepareDraftReplyEmail(draftReply); @@ -274,7 +312,10 @@ function EmailMessage({
{ onSendSuccess(messageId); diff --git a/apps/web/utils/actions/generate-reply.ts b/apps/web/utils/actions/generate-reply.ts new file mode 100644 index 0000000000..19cdd09d3c --- /dev/null +++ b/apps/web/utils/actions/generate-reply.ts @@ -0,0 +1,78 @@ +"use server"; + +import { auth } from "@/app/api/auth/[...nextauth]/auth"; +import { + generateReplySchema, + type GenerateReplySchema, +} from "@/utils/actions/generate-reply.validation"; +import { withActionInstrumentation } from "@/utils/actions/middleware"; +import { aiGenerateNudge } from "@/utils/ai/reply/generate-nudge"; +import { aiGenerateReply } from "@/utils/ai/reply/generate-reply"; +import { emailToContent } from "@/utils/mail"; +import { getAiUserByEmail } from "@/utils/user/get"; + +export const generateNudgeAction = withActionInstrumentation( + "generateNudge", + async (unsafeData: GenerateReplySchema) => { + const session = await auth(); + if (!session?.user.email) return { error: "Not authenticated" }; + + const user = await getAiUserByEmail({ email: session.user.email }); + + if (!user) return { error: "User not found" }; + + const { data, error } = generateReplySchema.safeParse(unsafeData); + if (error) return { error: error.message }; + + const lastMessage = data.messages.at(-1); + + if (!lastMessage) return { error: "No message provided" }; + + const messages = data.messages.map((msg) => ({ + ...msg, + date: new Date(msg.date), + content: emailToContent({ + textPlain: msg.textPlain, + textHtml: msg.textHtml, + snippet: "", + }), + })); + + const text = await aiGenerateNudge({ messages, user }); + + return { text }; + }, +); + +export const generateReplyAction = withActionInstrumentation( + "generateReply", + async (unsafeData: GenerateReplySchema) => { + const session = await auth(); + if (!session?.user.email) return { error: "Not authenticated" }; + + const user = await getAiUserByEmail({ email: session.user.email }); + + if (!user) return { error: "User not found" }; + + const { data, error } = generateReplySchema.safeParse(unsafeData); + if (error) return { error: error.message }; + + const lastMessage = data.messages.at(-1); + + if (!lastMessage) return { error: "No message provided" }; + + const messages = data.messages.map((msg) => ({ + ...msg, + date: new Date(msg.date), + content: emailToContent({ + textPlain: msg.textPlain, + textHtml: msg.textHtml, + snippet: "", + }), + })); + + const text = await aiGenerateReply({ messages, user }); + + return { text }; + }, +); diff --git a/apps/web/utils/actions/generate-reply.validation.ts b/apps/web/utils/actions/generate-reply.validation.ts new file mode 100644 index 0000000000..181ae2222d --- /dev/null +++ b/apps/web/utils/actions/generate-reply.validation.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +const messageSchema = z + .object({ + id: z.string(), + from: z.string(), + to: z.string(), + subject: z.string(), + textPlain: z.string().optional(), + textHtml: z.string().optional(), + date: z.string(), + }) + .refine((data) => data.textPlain || data.textHtml, { + message: "At least one of textPlain or textHtml is required", + }); + +export const generateReplySchema = z.object({ + messages: z.array(messageSchema), +}); + +export type GenerateReplySchema = z.infer; diff --git a/apps/web/utils/ai/reply/generate-nudge.ts b/apps/web/utils/ai/reply/generate-nudge.ts index bb413b0937..89b3c840bb 100644 --- a/apps/web/utils/ai/reply/generate-nudge.ts +++ b/apps/web/utils/ai/reply/generate-nudge.ts @@ -1,4 +1,4 @@ -import { chatCompletionStream } from "@/utils/llms"; +import { chatCompletion } from "@/utils/llms"; import type { UserEmailWithAI } from "@/utils/llms/types"; import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; @@ -8,7 +8,6 @@ const logger = createScopedLogger("generate-nudge"); export async function aiGenerateNudge({ messages, user, - onFinish, }: { messages: { from: string; @@ -41,17 +40,15 @@ Please write a follow-up email to nudge for a response.`; logger.trace("Input", { system, prompt }); - const response = await chatCompletionStream({ + const response = await chatCompletion({ userAi: user, system, prompt, userEmail: user.email, usageLabel: "Reply", - onFinish: async (completion) => { - logger.trace("Output", { completion }); - if (onFinish) await onFinish(completion); - }, }); - return response.toDataStreamResponse(); + logger.trace("Output", { response: response.text }); + + return response.text; } diff --git a/apps/web/utils/ai/reply/generate-reply.ts b/apps/web/utils/ai/reply/generate-reply.ts index 798c238883..2cd6e72176 100644 --- a/apps/web/utils/ai/reply/generate-reply.ts +++ b/apps/web/utils/ai/reply/generate-reply.ts @@ -1,9 +1,9 @@ -import { chatCompletionStream } from "@/utils/llms"; +import { chatCompletion } from "@/utils/llms"; import type { UserEmailWithAI } from "@/utils/llms/types"; import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; -// const logger = createScopedLogger("generate-reply"); +const logger = createScopedLogger("generate-reply"); export async function aiGenerateReply({ messages, @@ -36,7 +36,9 @@ ${stringifyEmail(msg, 3000)} Please write a reply to the email.`; - const response = await chatCompletionStream({ + logger.trace("Input", { system, prompt }); + + const response = await chatCompletion({ userAi: user, system, prompt, @@ -44,5 +46,7 @@ Please write a reply to the email.`; usageLabel: "Reply", }); - return response.toDataStreamResponse(); + logger.trace("Output", { response: response.text }); + + return response.text; } diff --git a/apps/web/utils/auth.ts b/apps/web/utils/auth.ts index 4683d6390b..90807ffab0 100644 --- a/apps/web/utils/auth.ts +++ b/apps/web/utils/auth.ts @@ -92,24 +92,24 @@ export const getAuthOptions: (options?: { return token; } - logger.info("JWT callback - current token state", { - email: token.email, - currentExpiresAt: token.expires_at - ? new Date((token.expires_at as number) * 1000).toISOString() - : "not set", - }); + // logger.info("JWT callback - current token state", { + // email: token.email, + // currentExpiresAt: token.expires_at + // ? new Date((token.expires_at as number) * 1000).toISOString() + // : "not set", + // }); if ( token.expires_at && Date.now() < (token.expires_at as number) * 1000 ) { - // If the access token has not expired yet, return it - logger.info("Token still valid", { - email: token.email, - expiresIn: - ((token.expires_at as number) * 1000 - Date.now()) / 1000 / 60, - minutes: true, - }); + // // If the access token has not expired yet, return it + // logger.info("Token still valid", { + // email: token.email, + // expiresIn: + // ((token.expires_at as number) * 1000 - Date.now()) / 1000 / 60, + // minutes: true, + // }); return token; } diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts index 0c2da2178b..5dedc876a3 100644 --- a/apps/web/utils/llms/index.ts +++ b/apps/web/utils/llms/index.ts @@ -108,6 +108,46 @@ function getModel({ aiProvider, aiModel, aiApiKey }: UserAIFields) { throw new Error("AI provider not supported"); } +export async function chatCompletion({ + userAi, + prompt, + system, + userEmail, + usageLabel, +}: { + userAi: UserAIFields; + prompt: string; + system?: string; + userEmail: string; + usageLabel: string; +}) { + try { + const { provider, model, llmModel } = getModel(userAi); + + const result = await generateText({ + model: llmModel, + prompt, + system, + experimental_telemetry: { isEnabled: true }, + }); + + if (result.usage) { + await saveAiUsage({ + email: userEmail, + usage: result.usage, + provider, + model, + label: usageLabel, + }); + } + + return result; + } catch (error) { + await handleError(error, userEmail); + throw error; + } +} + type ChatCompletionObjectArgs = { userAi: UserAIFields; prompt: string; From 396def0725bab8af960f92be849736e0a360bbde Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:50:13 +0200 Subject: [PATCH 08/20] move ai nudge to action --- apps/web/app/api/ai/reply/nudge/route.ts | 58 ------------------- apps/web/app/api/ai/reply/route.ts | 51 ---------------- .../web/components/email-list/EmailThread.tsx | 55 +++++++----------- apps/web/utils/actions/generate-reply.ts | 14 +++++ 4 files changed, 35 insertions(+), 143 deletions(-) delete mode 100644 apps/web/app/api/ai/reply/nudge/route.ts delete mode 100644 apps/web/app/api/ai/reply/route.ts diff --git a/apps/web/app/api/ai/reply/nudge/route.ts b/apps/web/app/api/ai/reply/nudge/route.ts deleted file mode 100644 index e78f33d928..0000000000 --- a/apps/web/app/api/ai/reply/nudge/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { z } from "zod"; -import { NextResponse } from "next/server"; -import { auth } from "@/app/api/auth/[...nextauth]/auth"; -import { withError } from "@/utils/middleware"; -import { aiGenerateNudge } from "@/utils/ai/reply/generate-nudge"; -import { getAiUserByEmail } from "@/utils/user/get"; -import { emailToContent } from "@/utils/mail"; - -const messageSchema = z - .object({ - id: z.string(), - from: z.string(), - to: z.string(), - subject: z.string(), - textPlain: z.string().optional(), - textHtml: z.string().optional(), - date: z.string(), - }) - .refine((data) => data.textPlain || data.textHtml, { - message: "At least one of textPlain or textHtml is required", - }); - -const generateReplyBody = z.object({ - messages: z.array(messageSchema), -}); - -export type GenerateReplyBody = z.infer; - -export const POST = withError(async (request: Request) => { - const session = await auth(); - if (!session?.user.email) - return NextResponse.json({ error: "Not authenticated" }); - - const user = await getAiUserByEmail({ email: session.user.email }); - - if (!user) return NextResponse.json({ error: "User not found" }); - - const json = await request.json(); - const body = generateReplyBody.parse(json); - - const lastMessage = body.messages.at(-1); - - if (!lastMessage) return NextResponse.json({ error: "No message provided" }); - - const messages = body.messages.map((msg) => ({ - ...msg, - date: new Date(msg.date), - content: emailToContent({ - textPlain: msg.textPlain, - textHtml: msg.textHtml, - snippet: "", - }), - })); - - const text = await aiGenerateNudge({ messages, user }); - - return NextResponse.json({ text }); -}); diff --git a/apps/web/app/api/ai/reply/route.ts b/apps/web/app/api/ai/reply/route.ts deleted file mode 100644 index 07b08d0a9d..0000000000 --- a/apps/web/app/api/ai/reply/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { z } from "zod"; -import { NextResponse } from "next/server"; -import { auth } from "@/app/api/auth/[...nextauth]/auth"; -import { withError } from "@/utils/middleware"; -import { aiGenerateReply } from "@/utils/ai/reply/generate-reply"; -import { getAiUserByEmail } from "@/utils/user/get"; -import { emailToContent } from "@/utils/mail"; - -const messageSchema = z - .object({ - from: z.string(), - to: z.string(), - subject: z.string(), - textPlain: z.string().optional(), - textHtml: z.string().optional(), - date: z.string(), - }) - .refine((data) => data.textPlain || data.textHtml, { - message: "At least one of textPlain or textHtml is required", - }); - -const generateReplyBody = z.object({ - messages: z.array(messageSchema), -}); - -export const POST = withError(async (request: Request) => { - const session = await auth(); - if (!session?.user.email) - return NextResponse.json({ error: "Not authenticated" }); - - const user = await getAiUserByEmail({ email: session.user.email }); - - if (!user) return NextResponse.json({ error: "User not found" }); - - const json = await request.json(); - const body = generateReplyBody.parse(json); - - const messages = body.messages.map((msg) => ({ - ...msg, - date: new Date(msg.date), - content: emailToContent({ - textPlain: msg.textPlain, - textHtml: msg.textHtml, - snippet: "", - }), - })); - - const stream = await aiGenerateReply({ messages, user }); - - return stream; -}); diff --git a/apps/web/components/email-list/EmailThread.tsx b/apps/web/components/email-list/EmailThread.tsx index 354041095a..2150639218 100644 --- a/apps/web/components/email-list/EmailThread.tsx +++ b/apps/web/components/email-list/EmailThread.tsx @@ -29,8 +29,7 @@ import { extractEmailReply } from "@/utils/parse/extract-reply.client"; import type { ReplyingToEmail } from "@/app/(app)/compose/ComposeEmailForm"; import { createReplyContent } from "@/utils/gmail/reply"; import { cn } from "@/utils"; -import { useCompletion } from "ai/react"; -import type { GenerateReplyBody } from "@/app/api/ai/reply/nudge/route"; +import { generateNudgeAction } from "@/utils/actions/generate-reply"; type EmailMessage = Thread["messages"][number]; @@ -166,36 +165,27 @@ function EmailMessage({ setShowForward(false); }, []); - const body: GenerateReplyBody = { - messages: [ - { - id: message.id, - textHtml: message.textHtml, - textPlain: message.textPlain, - date: message.headers.date, - from: message.headers.from, - to: message.headers.to, - subject: message.headers.subject, - }, - ], - }; - - const { completion, complete, error, isLoading } = useCompletion({ - api: "/api/ai/reply/nudge", - body, - }); - console.log("🚀 ~ completion:", completion); - - if (error) { - console.error("There was an error generating the nudge", error); - } - useEffect(() => { - if (generateNudge) { - // we send the data via the body instead - complete(""); + async function loadNudge() { + const result = await generateNudgeAction({ + messages: [ + { + id: message.id, + textHtml: message.textHtml, + textPlain: message.textPlain, + date: message.headers.date, + from: message.headers.from, + to: message.headers.to, + subject: message.headers.subject, + }, + ], + }); + + console.log("🚀 ~ result:", result); } - }, [complete, generateNudge]); + + if (generateNudge) loadNudge(); + }, [generateNudge, message]); const replyingToEmail: ReplyingToEmail = useMemo(() => { if (showReply) { @@ -312,10 +302,7 @@ function EmailMessage({
{ onSendSuccess(messageId); diff --git a/apps/web/utils/actions/generate-reply.ts b/apps/web/utils/actions/generate-reply.ts index 19cdd09d3c..c402fb8303 100644 --- a/apps/web/utils/actions/generate-reply.ts +++ b/apps/web/utils/actions/generate-reply.ts @@ -9,6 +9,7 @@ import { withActionInstrumentation } from "@/utils/actions/middleware"; import { aiGenerateNudge } from "@/utils/ai/reply/generate-nudge"; import { aiGenerateReply } from "@/utils/ai/reply/generate-reply"; import { emailToContent } from "@/utils/mail"; +import { getReply, saveReply } from "@/utils/redis/reply"; import { getAiUserByEmail } from "@/utils/user/get"; export const generateNudgeAction = withActionInstrumentation( @@ -61,6 +62,13 @@ export const generateReplyAction = withActionInstrumentation( if (!lastMessage) return { error: "No message provided" }; + const reply = await getReply({ + userId: user.id, + messageId: lastMessage.id, + }); + + if (reply) return { text: reply }; + const messages = data.messages.map((msg) => ({ ...msg, date: new Date(msg.date), @@ -73,6 +81,12 @@ export const generateReplyAction = withActionInstrumentation( const text = await aiGenerateReply({ messages, user }); + await saveReply({ + userId: user.id, + messageId: lastMessage.id, + reply: text, + }); + return { text }; }, ); From e610ef99c532b19bf100f3de37ec06d3ccd11db4 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:52:31 +0200 Subject: [PATCH 09/20] cache nudge --- apps/web/utils/actions/generate-reply.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/web/utils/actions/generate-reply.ts b/apps/web/utils/actions/generate-reply.ts index c402fb8303..41d948365f 100644 --- a/apps/web/utils/actions/generate-reply.ts +++ b/apps/web/utils/actions/generate-reply.ts @@ -29,6 +29,13 @@ export const generateNudgeAction = withActionInstrumentation( if (!lastMessage) return { error: "No message provided" }; + const reply = await getReply({ + userId: user.id, + messageId: lastMessage.id, + }); + + if (reply) return { text: reply }; + const messages = data.messages.map((msg) => ({ ...msg, date: new Date(msg.date), @@ -41,6 +48,12 @@ export const generateNudgeAction = withActionInstrumentation( const text = await aiGenerateNudge({ messages, user }); + await saveReply({ + userId: user.id, + messageId: lastMessage.id, + reply: text, + }); + return { text }; }, ); From b80289306370db0b7f4c097ceda3731aadb751a1 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:59:58 +0200 Subject: [PATCH 10/20] break down email thread into smaller components --- .../email-list/EmailAttachments.tsx | 62 +++ .../components/email-list/EmailContents.tsx | 67 +++ .../components/email-list/EmailDetails.tsx | 31 ++ .../components/email-list/EmailMessage.tsx | 248 +++++++++++ .../web/components/email-list/EmailThread.tsx | 415 +----------------- apps/web/components/email-list/types.ts | 2 + 6 files changed, 416 insertions(+), 409 deletions(-) create mode 100644 apps/web/components/email-list/EmailAttachments.tsx create mode 100644 apps/web/components/email-list/EmailContents.tsx create mode 100644 apps/web/components/email-list/EmailDetails.tsx create mode 100644 apps/web/components/email-list/EmailMessage.tsx diff --git a/apps/web/components/email-list/EmailAttachments.tsx b/apps/web/components/email-list/EmailAttachments.tsx new file mode 100644 index 0000000000..064817ee81 --- /dev/null +++ b/apps/web/components/email-list/EmailAttachments.tsx @@ -0,0 +1,62 @@ +import { Card } from "@/components/Card"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { DownloadIcon } from "lucide-react"; +import type { ThreadMessage } from "@/components/email-list/types"; + +export function EmailAttachments({ message }: { message: ThreadMessage }) { + return ( +
+ {message.attachments?.map((attachment) => { + const url = `/api/google/messages/attachment?messageId=${message.id}&attachmentId=${attachment.attachmentId}&mimeType=${attachment.mimeType}&filename=${attachment.filename}`; + + return ( + +
{attachment.filename}
+
+
+ {mimeTypeToString(attachment.mimeType)} +
+ +
+
+ ); + })} +
+ ); +} + +function mimeTypeToString(mimeType: string): string { + switch (mimeType) { + case "application/pdf": + return "PDF"; + case "application/zip": + return "ZIP"; + case "image/png": + return "PNG"; + case "image/jpeg": + return "JPEG"; + // LLM generated. Need to check they're actually needed + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + return "DOCX"; + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + return "XLSX"; + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + return "PPTX"; + case "application/vnd.ms-excel": + return "XLS"; + case "application/vnd.ms-powerpoint": + return "PPT"; + case "application/vnd.ms-word": + return "DOC"; + default: + return mimeType; + } +} diff --git a/apps/web/components/email-list/EmailContents.tsx b/apps/web/components/email-list/EmailContents.tsx new file mode 100644 index 0000000000..0c284f690c --- /dev/null +++ b/apps/web/components/email-list/EmailContents.tsx @@ -0,0 +1,67 @@ +import { type SyntheticEvent, useCallback, useMemo, useState } from "react"; +import { Loading } from "@/components/Loading"; + +export function HtmlEmail({ html }: { html: string }) { + const srcDoc = useMemo(() => getIframeHtml(html), [html]); + const [isLoading, setIsLoading] = useState(true); + + const onLoad = useCallback( + (event: SyntheticEvent) => { + if (event.currentTarget.contentWindow) { + // sometimes we see minimal scrollbar, so add a buffer + const BUFFER = 5; + + const height = `${ + event.currentTarget.contentWindow.document.documentElement + .scrollHeight + BUFFER + }px`; + + event.currentTarget.style.height = height; + setIsLoading(false); + } + }, + [], + ); + + return ( +
+ {isLoading && } +