diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/digest-frequency/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/digest-frequency/page.tsx index eb8eb40bf8..bf529b1771 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/digest-frequency/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/digest-frequency/page.tsx @@ -6,7 +6,11 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useState, useEffect } from "react"; -import { updateDigestCategoriesAction } from "@/utils/actions/settings"; +import { + updateDigestCategoriesAction, + updateDigestScheduleAction, + ensureDefaultDigestScheduleAction, +} from "@/utils/actions/settings"; import { toastError, toastSuccess } from "@/components/Toast"; import { prefixPath } from "@/utils/path"; import { useAccount } from "@/providers/EmailAccountProvider"; @@ -53,6 +57,13 @@ export default function DigestFrequencyPage() { } }, [digestSettings]); + useEffect(() => { + // Ensure user has a digest schedule entry when they visit this page, otherwise the digest is not sent + const timeOfDay = new Date(); + timeOfDay.setHours(11, 0, 0, 0); // 11 AM in user's timezone + ensureDefaultDigestScheduleAction(emailAccountId, { timeOfDay }); + }, [emailAccountId]); + const updateDigestCategories = updateDigestCategoriesAction.bind( null, emailAccountId, @@ -68,19 +79,43 @@ export default function DigestFrequencyPage() { const handleFinish = async () => { setIsLoading(true); try { + // Save digest categories const result = await updateDigestCategories(settings); if (result?.serverError) { toastError({ description: "Failed to save digest settings. Please try again.", }); - } else { - toastSuccess({ description: "Digest settings saved!" }); - markOnboardingAsCompleted(ASSISTANT_ONBOARDING_COOKIE); - router.push( - prefixPath(emailAccountId, "/assistant/onboarding/completed"), - ); + return; } + + // Ensure a default digest schedule is set if none exists + const updateDigestSchedule = updateDigestScheduleAction.bind( + null, + emailAccountId, + ); + + const scheduleResult = await updateDigestSchedule({ + schedule: { + intervalDays: 7, + daysOfWeek: 1 << (6 - 1), // Monday (1) + timeOfDay: new Date(new Date().setHours(11, 0, 0, 0)), // 11 AM + occurrences: 1, + }, + }); + + if (scheduleResult?.serverError) { + toastError({ + description: "Failed to set digest frequency. Please try again.", + }); + return; + } + + toastSuccess({ description: "Digest settings saved!" }); + markOnboardingAsCompleted(ASSISTANT_ONBOARDING_COOKIE); + router.push( + prefixPath(emailAccountId, "/assistant/onboarding/completed"), + ); } catch (error) { toastError({ description: "Failed to save digest settings. Please try again.", diff --git a/apps/web/utils/actions/settings.ts b/apps/web/utils/actions/settings.ts index 5fd71394b2..11de848904 100644 --- a/apps/web/utils/actions/settings.ts +++ b/apps/web/utils/actions/settings.ts @@ -1,5 +1,6 @@ "use server"; +import { z } from "zod"; import { actionClient } from "@/utils/actions/safe-action"; import { saveAiSettingsBody, @@ -195,3 +196,42 @@ export const updateDigestCategoriesAction = actionClient return { success: true }; }, ); + +export const ensureDefaultDigestScheduleAction = actionClient + .metadata({ name: "ensureDefaultDigestSchedule" }) + .schema(z.object({ timeOfDay: z.date() })) + .action(async ({ ctx: { emailAccountId }, parsedInput: { timeOfDay } }) => { + try { + // Check if user already has a digest schedule + const existingSchedule = await prisma.schedule.findUnique({ + where: { emailAccountId }, + select: { id: true }, + }); + + // If no schedule exists, create a default one + if (!existingSchedule) { + const defaultSchedule = { + intervalDays: 7, + daysOfWeek: 1 << (6 - 1), // Monday (bit 5 set = Monday) + timeOfDay: timeOfDay, // Use provided date/time from user to set the accurate users timezone + occurrences: 1, + }; + + await prisma.schedule.create({ + data: { + emailAccountId, + intervalDays: defaultSchedule.intervalDays, + daysOfWeek: defaultSchedule.daysOfWeek, + timeOfDay: defaultSchedule.timeOfDay, + occurrences: defaultSchedule.occurrences, + lastOccurrenceAt: new Date(), + nextOccurrenceAt: calculateNextScheduleDate(defaultSchedule), + }, + }); + } + + return { success: true }; + } catch (error) { + throw new SafeError("Failed to ensure digest schedule", 500); + } + }); 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 1cca772bb0..8b3029561a 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts @@ -30,6 +30,8 @@ async function getAiResponse(options: GetAiResponseOptions) { 1. Match the email to a SPECIFIC user-defined rule that addresses the email's exact content or purpose. 2. If the email doesn't match any specific rule but the user has a catch-all rule (like "emails that don't match other criteria"), use that catch-all rule. 3. Only set "noMatchFound" to true if no user-defined rule can reasonably apply. + 4. Be concise in your reasoning - avoid repetitive explanations. + 5. Provide only the exact rule name from the list below. @@ -62,7 +64,7 @@ ${ } -Respond with a JSON object with the following fields: +Respond with a valid JSON object with the following fields: "reason" - the reason you chose that rule. Keep it concise. "ruleName" - the exact name of the rule you want to apply "noMatchFound" - true if no match was found, false otherwise diff --git a/packages/resend/emails/digest.tsx b/packages/resend/emails/digest.tsx index df77f54122..17ef10a793 100644 --- a/packages/resend/emails/digest.tsx +++ b/packages/resend/emails/digest.tsx @@ -204,7 +204,7 @@ export default function DigestEmail(props: DigestEmailProps) { {category.emoji} {category.name}