Skip to content

AI generate reply / nudge endpoints#330

Merged
elie222 merged 20 commits intomainfrom
generate-reply
Feb 10, 2025
Merged

AI generate reply / nudge endpoints#330
elie222 merged 20 commits intomainfrom
generate-reply

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Feb 9, 2025

Summary by CodeRabbit

  • New Features

    • Introduced new functions for generating AI-powered follow-up nudges and automated email replies to enhance communication efficiency.
    • Added a new component for displaying email attachments.
    • Implemented components for rendering HTML and plain text email content, improving email presentation.
    • Introduced a new component for displaying detailed email information.
    • Added a new asynchronous action for generating replies with improved validation.
    • Enhanced the email categorization process to utilize internal dates for improved accuracy.
    • Modified the email thread component to support a new message type and improved reply generation.
  • Refactor

    • Standardized date handling across various components and utilities for clearer, more consistent interactions.
    • Streamlined the email thread view and improved layout for better user experience.
  • Chores

    • Upgraded dependency versions for improved stability and reliability.

@vercel
Copy link

vercel bot commented Feb 9, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
inbox-zero ❌ Failed (Inspect) Feb 10, 2025 4:04pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This pull request introduces a series of updates across the codebase. It removes an unused import in the summarization controller and adds new asynchronous functions for generating AI nudges and replies. Several import paths have been restructured to consolidate utility functions, with significant updates to date handling using the new internalDateToDate function. The changes also include UI modifications for email date display, schema updates in validation files, and dependency version locks in package configuration files. Minor adjustments in logging and type definitions complement the overall refactoring.

Changes

File(s) Summary of Changes
apps/web/app/api/ai/summarise/controller.ts
apps/web/app/api/ai/categorize/validation.ts
apps/web/app/api/google/webhook/process-history-item.ts
apps/web/app/api/user/stats/tinybird/load/load-emails.ts
Removed unused import in summarization; updated schema property from date to internalDate; modified date processing using internalDateToDate in API endpoints.
apps/web/utils/ai/reply/generate-nudge.ts
apps/web/utils/ai/reply/generate-reply.ts
apps/web/utils/actions/generate-reply.ts
apps/web/utils/actions/generate-reply.validation.ts
Added new asynchronous functions for generating nudges and replies with prompt construction, logging, and validation using a new Zod schema.
apps/web/utils/ai/assistant/process-user-request.ts
apps/web/utils/ai/choose-rule/*
apps/web/utils/cold-email/is-cold-email.ts
apps/web/utils/reply-tracker/*
apps/web/utils/get-email-from-message.ts
apps/web/utils/gmail/message.ts
apps/web/utils/types.ts
apps/web/utils/actions/categorize-email.ts
apps/web/utils/actions/cold-email.ts
apps/web/utils/assistant/process-assistant-email.ts
Updated multiple import paths to new consolidated module locations; introduced and integrated internalDateToDate for uniform date conversion; updated function signatures and parameter types from strings to Date where applicable.
apps/web/utils/user/get.ts Added a new asynchronous function getAiUserByEmail to retrieve user details using Prisma.
apps/web/app/(app)/cold-email-blocker/TestRules.tsx
apps/web/app/(app)/simple/SimpleList.tsx
apps/web/components/email-list/EmailThread.tsx
apps/web/components/email-list/EmailList.tsx
Modified UI components: added a new date property in test calls, removed the displayed email date, and introduced a generateNudge prop with associated logic in email threads.
apps/unsubscriber/package.json
apps/web/package.json
Updated dependency versions from caret notation (^) to exact versions for multiple AI SDK packages and the ai library.
apps/web/providers/SWRProvider.tsx
apps/web/utils/auth.ts
apps/web/utils/llms/index.ts
Added a conditional check in the fetcher to bypass /api/ai/ calls; reduced JWT logging verbosity; added a new chatCompletion function for AI text generation.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Email Component
  participant GN as generateNudgeAction
  participant Auth as auth
  participant UserDB as getAiUserByEmail
  participant Chat as chatCompletion
  UI->>GN: Request nudge generation with message data
  GN->>Auth: Authenticate user
  Auth-->>GN: Return user credentials
  GN->>UserDB: Fetch AI user details by email
  UserDB-->>GN: Return user information
  GN->>Chat: Call chatCompletion with prompt & data
  Chat-->>GN: Return generated nudge text
  GN-->>UI: Deliver nudge response
Loading
sequenceDiagram
  participant UI as Email Component
  participant GR as generateReplyAction
  participant Auth as auth
  participant UserDB as getAiUserByEmail
  participant DB as Reply Database
  participant Chat as chatCompletion
  UI->>GR: Request reply generation with thread messages
  GR->>Auth: Check user authentication
  Auth-->>GR: Validated credentials
  GR->>UserDB: Retrieve AI user details
  UserDB-->>GR: Return user data
  GR->>DB: Query for existing reply
  alt Reply Found
    DB-->>GR: Return existing reply
  else No Reply Found
    GR->>Chat: Request reply generation with formatted messages
    Chat-->>GR: Return new reply text
    GR->>DB: Save generated reply
  end
  GR-->>UI: Return reply text
Loading

Possibly related PRs

  • Fix cold email blocker on previous messages from sender #328: The changes in the main PR, which involve the removal of an import statement and the preservation of the summarise function's behavior, are related to the retrieved PR, which adds a messageId property to various functions and structures, including the testEmail function, indicating a focus on message identification that aligns with the overall handling of email data.
  • Fix bug with not handling cold email follow up properly #322: The changes in the main PR, which involve the removal of an import statement and adjustments to the handling of the date property in various functions, are related to the modifications in the retrieved PR that also focus on the handling of the date field in the context of cold email processing, specifically in the runColdEmailBlocker function. Both PRs involve changes to how the date property is utilized in their respective functions.

Poem

I hopped through lines of code with glee,
Updating prompts and functions merrily.
Dates now dance with internalDateToDate,
Imports realigned, every module in the right slot.
With nudges and replies borne on the breeze,
This rabbit celebrates refactoring with ease!
🐇💻


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a67166 and d186e8d.

📒 Files selected for processing (4)
  • apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx (3 hunks)
  • apps/web/components/email-list/EmailAttachments.tsx (1 hunks)
  • apps/web/hooks/useThreadsByIds.ts (1 hunks)
  • apps/web/providers/SWRProvider.tsx (1 hunks)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
apps/web/app/api/ai/reply/nudge/route.ts (2)

8-19: Consider validating the date field.
Currently, the schema declares 'date' as just a string. For better robustness, you might want to ensure it's a valid date format or use zod’s built-in date validation (if available in your version) to avoid potential runtime errors when parsing an invalid date string.


41-41: Resolve the TODO for parsing HTML content.
The code references parsing HTML content but is not implemented. This might cause issues if only textHtml is populated.

Would you like me to propose a snippet to extract the text from HTML or handle the rendered HTML directly?

apps/web/utils/ai/reply/generate-reply.ts (1)

27-37: Be aware of potential prompt size limits.
Concatenating multiple emails into a single prompt can exceed token limits. Consider truncating or summarizing older parts of the conversation more aggressively if messages become huge.

apps/web/utils/ai/reply/generate-nudge.ts (3)

6-6: Uncomment the logger setup.

The logger is commented out but could be useful for debugging and monitoring in production.

-// const logger = createScopedLogger("generate-nudge");
+const logger = createScopedLogger("generate-nudge");

8-20: Consider adding input validation.

The function accepts messages without validating their content length or format. Consider adding validation to ensure the input data meets your requirements before processing.

export async function aiGenerateNudge({
  messages,
  user,
}: {
  messages: {
    from: string;
    to: string;
    subject: string;
    content: string;
    date: Date;
  }[];
  user: UserEmailWithAI;
+}) {
+  if (!messages.length) {
+    throw new Error("At least one message is required");
+  }
+
+  if (!messages.every(msg => msg.content.trim())) {
+    throw new Error("All messages must have non-empty content");
+  }
}) {

21-25: Consider parameterizing the system prompt.

The system prompt is hardcoded. Consider making it configurable or moving it to a constants file for better maintainability.

+const NUDGE_SYSTEM_PROMPT = `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.`;

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 system = NUDGE_SYSTEM_PROMPT;
apps/web/app/api/ai/reply/route.ts (2)

8-19: Consider adding more validation constraints.

The message schema could benefit from additional validation:

  • Email format validation for from and to
  • Maximum length constraints for subject and content fields
  • Date format validation
const messageSchema = z
  .object({
-    from: z.string(),
-    to: z.string(),
+    from: z.string().email(),
+    to: z.string().email(),
-    subject: z.string(),
+    subject: z.string().max(200),
-    textPlain: z.string().optional(),
-    textHtml: z.string().optional(),
+    textPlain: z.string().max(50000).optional(),
+    textHtml: z.string().max(100000).optional(),
-    date: z.string(),
+    date: z.string().refine((val) => !isNaN(Date.parse(val)), {
+      message: "Invalid date format",
+    }),
  })

25-48: Consider rate limiting and response size limits.

The API endpoint should implement rate limiting and set maximum response size limits to prevent abuse.

Consider using a middleware for rate limiting:

import rateLimit from 'express-rate-limit'

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

export const POST = withError(
  limiter(async (request: Request) => {
    // ... existing code ...
  })
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6827bdf and 64fdcf4.

📒 Files selected for processing (6)
  • apps/web/app/api/ai/reply/nudge/route.ts (1 hunks)
  • apps/web/app/api/ai/reply/route.ts (1 hunks)
  • apps/web/app/api/ai/summarise/controller.ts (0 hunks)
  • apps/web/utils/ai/reply/generate-nudge.ts (1 hunks)
  • apps/web/utils/ai/reply/generate-reply.ts (1 hunks)
  • apps/web/utils/user/get.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/app/api/ai/summarise/controller.ts
🔇 Additional comments (5)
apps/web/app/api/ai/reply/nudge/route.ts (1)

47-47: Confirm the streaming response aligns with Next.js conventions.
Returning the result directly as a stream is likely correct for your streaming use case; just verify that Next.js (or your framework) supports streaming responses in all runtime environments (e.g., Edge, serverful environments).

apps/web/utils/user/get.ts (1)

1-15: Validate uniqueness of the user email for consistent findUnique usage.
If multiple users share the same email (which is a rare edge case), findUnique could fail. Confirm that there is a unique index on the 'email' column to guarantee correct behavior.

apps/web/utils/ai/reply/generate-reply.ts (2)

21-25: Check for brand consistency and legal disclaimers in AI-generated emails.
Your system prompts the AI to refrain from mentioning it’s an AI. Ensure that this approach aligns with business/legal guidelines.


39-47: Verify usage label and streaming completion flow.
Labeling usage as "Reply" is a good start; confirm in your analytics or logs that this usage label is properly tracked. Also ensure the streaming flow handles partial or truncated responses gracefully if tokens run out.

apps/web/app/api/ai/reply/route.ts (1)

41-42: Address the TODO comment about HTML parsing.

The code currently uses raw HTML content as a fallback. Consider implementing proper HTML parsing to extract clean text content.

Would you like me to help implement HTML parsing using a library like html-to-text?

Comment on lines 25 to 28
export const POST = withError(async (request: Request) => {
const session = await auth();
if (!session?.user.email)
return NextResponse.json({ error: "Not authenticated" });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return proper HTTP status codes for errors.
Returning JSON with an “error” field and defaulting to HTTP 200 can confuse clients expecting 401 for unauthorized or 404 for user-not-found. Consider adding status codes to give a precise indication of the failure reason.

Comment on lines 39 to 45
const response = await chatCompletionStream({
userAi: user,
system,
prompt,
userEmail: user.email,
usageLabel: "Reply",
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding error handling for chatCompletionStream.

The function should handle potential errors from the chatCompletionStream call.

+  try {
     const response = await chatCompletionStream({
       userAi: user,
       system,
       prompt,
       userEmail: user.email,
       usageLabel: "Reply",
     });
+    return response.toTextStreamResponse();
+  } catch (error) {
+    logger.error("Error generating nudge", { error });
+    throw new Error("Failed to generate nudge email");
+  }
-    return response.toTextStreamResponse();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await chatCompletionStream({
userAi: user,
system,
prompt,
userEmail: user.email,
usageLabel: "Reply",
});
try {
const response = await chatCompletionStream({
userAi: user,
system,
prompt,
userEmail: user.email,
usageLabel: "Reply",
});
return response.toTextStreamResponse();
} catch (error) {
logger.error("Error generating nudge", { error });
throw new Error("Failed to generate nudge email");
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/web/app/api/user/stats/tinybird/load/load-emails.ts (1)

182-189: Consider adding fallback for timestamp.

The current implementation skips emails without a timestamp. Consider adding a fallback mechanism or logging more context for debugging.

 if (!tinybirdEmail.timestamp) {
   logger.error("No timestamp for email", {
     ownerEmail: tinybirdEmail.ownerEmail,
     gmailMessageId: tinybirdEmail.gmailMessageId,
     date: m.internalDate,
+    headers: m.headers,
+    fallbackTimestamp: +new Date()
   });
-  return;
+  tinybirdEmail.timestamp = +new Date(); // Use current timestamp as fallback
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37097d4 and aec6cd3.

📒 Files selected for processing (13)
  • apps/web/app/(app)/cold-email-blocker/TestRules.tsx (1 hunks)
  • apps/web/app/(app)/simple/SimpleList.tsx (0 hunks)
  • apps/web/app/api/ai/categorize/validation.ts (1 hunks)
  • apps/web/app/api/google/webhook/process-history-item.ts (2 hunks)
  • apps/web/app/api/user/stats/tinybird/load/load-emails.ts (3 hunks)
  • apps/web/components/email-list/EmailList.tsx (1 hunks)
  • apps/web/utils/actions/categorize-email.ts (2 hunks)
  • apps/web/utils/actions/cold-email.ts (1 hunks)
  • apps/web/utils/assistant/process-assistant-email.ts (2 hunks)
  • apps/web/utils/cold-email/is-cold-email.ts (3 hunks)
  • apps/web/utils/get-email-from-message.ts (2 hunks)
  • apps/web/utils/gmail/message.ts (2 hunks)
  • apps/web/utils/types.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/app/(app)/simple/SimpleList.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/utils/cold-email/is-cold-email.ts
🔇 Additional comments (10)
apps/web/utils/get-email-from-message.ts (1)

3-3: LGTM! Good improvement in date handling.

The changes standardize date handling by using Gmail's internal date instead of the email header date, which is more reliable.

Also applies to: 15-15

apps/web/app/api/ai/categorize/validation.ts (1)

20-20: LGTM! Consistent with date handling changes.

The schema now correctly uses internalDate from Gmail's API instead of the email header date.

apps/web/utils/actions/categorize-email.ts (1)

16-16: LGTM! Good improvements in code structure and type safety.

The changes:

  1. Use object parameter pattern for better readability
  2. Convert string date to Date object for type safety

Also applies to: 43-47

apps/web/utils/types.ts (1)

96-96: LGTM! Good documentation.

The comment clearly explains the date handling strategy, distinguishing between email header date and Gmail's internal date.

apps/web/utils/actions/cold-email.ts (1)

125-125: LGTM! Consistent date handling.

The change ensures type safety and consistent date handling by explicitly converting the date string to a Date object.

apps/web/utils/gmail/message.ts (1)

104-104: LGTM! Enhanced type safety for date parameters.

The function signatures now correctly specify Date objects instead of strings, improving type safety and consistency.

Also applies to: 136-136

apps/web/app/api/user/stats/tinybird/load/load-emails.ts (1)

173-173: LGTM! Improved date handling with dedicated utility.

Using internalDateToDate provides consistent date handling across the codebase.

Also applies to: 186-186

apps/web/app/api/google/webhook/process-history-item.ts (1)

142-142: LGTM! Consistent date handling in cold email blocker.

The change aligns with the codebase-wide standardization of date handling using internalDateToDate.

apps/web/app/(app)/cold-email-blocker/TestRules.tsx (1)

171-171: LGTM! Consistent date handling.

The change aligns with the codebase-wide shift to using internalDate for date handling.

apps/web/utils/assistant/process-assistant-email.ts (1)

13-13: LGTM! Standardized date handling.

The changes improve consistency by using the centralized internalDateToDate utility for date conversions.

Also applies to: 173-175, 178-180

cc?: string;
subject: string;
content: string;
date?: Date;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify if date should be optional.

The date property is marked as optional (date?: Date). Given that internalDate is required in the categorization schema, should this be required here as well?

Let's check how this type is used:


🏁 Script executed:

#!/bin/bash
# Search for usages of EmailForLLM type and date property
rg -A 5 "EmailForLLM" && rg "date.*Date" --type ts

Length of output: 20626


Action: Remove the optional modifier on the “date” property.

After reviewing the usage across the codebase and seeing that functions like getEmailForLLM always provide a date value (whether as a string via ISO format or as a parsed Date), it’s clear that consumers are built with the assumption that a date is always present. In the type definition (in apps/web/utils/types.ts), the property appears twice—once as a required string and again as an optional Date—which is both redundant and potentially error‑prone given that the categorization schema requires an internal date. The fix is to remove the optional modifier (or refactor to use differently named properties if both representations are needed).

  • Location: apps/web/utils/types.ts (definition of EmailForLLM)
  • Issue: Duplicate declaration of date exists (a required string and an optional Date) causing inconsistency with the categorization schema's expectation that a date always be provided.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/web/utils/ai/reply/generate-nudge.ts (1)

8-55: Consider extracting common code into a shared utility.

The aiGenerateNudge function shares significant code with aiGenerateReply. Consider extracting the common functionality into a shared utility to improve maintainability.

// shared-email-generator.ts
interface EmailGeneratorConfig {
  messages: EmailMessage[];
  user: UserEmailWithAI;
  systemPrompt: string;
  userPrompt: string;
  onFinish?: (completion: string) => Promise<void>;
}

async function generateEmail({
  messages,
  user,
  systemPrompt,
  userPrompt,
  onFinish,
}: EmailGeneratorConfig) {
  // Common validation, formatting, and generation logic
}

// generate-nudge.ts
export async function aiGenerateNudge({ messages, user, onFinish }) {
  return generateEmail({
    messages,
    user,
    systemPrompt: `You are an AI assistant...`,
    userPrompt: `Please write a follow-up email...`,
    onFinish,
  });
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3d6e6c and 41e6a1e.

📒 Files selected for processing (5)
  • apps/web/app/api/ai/reply/nudge/route.ts (1 hunks)
  • apps/web/app/api/ai/reply/route.ts (1 hunks)
  • apps/web/providers/SWRProvider.tsx (1 hunks)
  • apps/web/utils/ai/reply/generate-nudge.ts (1 hunks)
  • apps/web/utils/ai/reply/generate-reply.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/api/ai/reply/route.ts
🔇 Additional comments (3)
apps/web/utils/ai/reply/generate-reply.ts (2)

8-20: Add input validation for messages array.

The function should validate that the messages array is not empty and properly ordered by date to ensure correct context flow.

if (!messages.length) {
  throw new Error("No messages provided");
}

// Verify messages are ordered by date
const isOrdered = messages.every((msg, i) => 
  i === 0 || msg.date >= messages[i-1].date
);
if (!isOrdered) {
  throw new Error("Messages must be ordered by date");
}

39-49: Add error handling for chatCompletionStream.

The function should handle potential errors from the chatCompletionStream call.

+  try {
     const response = await chatCompletionStream({
       userAi: user,
       system,
       prompt,
       userEmail: user.email,
       usageLabel: "Reply",
       onFinish: async (completion) => {
         logger.trace("Output", { completion });
       },
     });
+    return response.toDataStreamResponse();
+  } catch (error) {
+    logger.error("Error generating reply", { error });
+    throw new Error("Failed to generate reply email");
+  }
-    return response.toDataStreamResponse();
apps/web/app/api/ai/reply/nudge/route.ts (1)

29-33: Return proper HTTP status codes for errors.

Return appropriate HTTP status codes instead of defaulting to 200.

-    return NextResponse.json({ error: "Not authenticated" });
+    return NextResponse.json(
+      { error: "Not authenticated" },
+      { status: 401 }
+    );

Comment on lines 34 to 37
const user = await getAiUserByEmail({ email: session.user.email });

if (!user) return NextResponse.json({ error: "User not found" });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add proper error handling for database operations.

The getAiUserByEmail call should be wrapped in a try-catch block to handle potential database errors.

+  try {
     const user = await getAiUserByEmail({ email: session.user.email });
     if (!user) return NextResponse.json(
       { error: "User not found" },
       { status: 404 }
     );
+  } catch (error) {
+    logger.error("Database error", { error });
+    return NextResponse.json(
+      { error: "Internal server error" },
+      { status: 500 }
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const user = await getAiUserByEmail({ email: session.user.email });
if (!user) return NextResponse.json({ error: "User not found" });
try {
const user = await getAiUserByEmail({ email: session.user.email });
if (!user)
return NextResponse.json(
{ error: "User not found" },
{ status: 404 }
);
} catch (error) {
logger.error("Database error", { error });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/web/utils/ai/reply/generate-nudge.ts (1)

43-50: Add error handling for chatCompletion call.

The function should handle potential errors from the chatCompletion call.

-  const response = await chatCompletion({
-    userAi: user,
-    system,
-    prompt,
-    userEmail: user.email,
-    usageLabel: "Reply",
-  });
+  try {
+    const response = await chatCompletion({
+      userAi: user,
+      system,
+      prompt,
+      userEmail: user.email,
+      usageLabel: "Reply",
+    });
+    return response.text;
+  } catch (error) {
+    logger.error("Error generating nudge", { error });
+    throw new Error("Failed to generate nudge email");
+  }
-  return response.text;
🧹 Nitpick comments (4)
apps/web/components/email-list/EmailThread.tsx (1)

315-318: Potential content overwriting
Using completion || replyingToEmail.draftHtml might overwrite the user's typed draft if a new AI completion is available. Confirm that you want to discard manual changes if there's a valid completion. Alternatively, consider merging both inputs or prompting user confirmation.

apps/web/utils/actions/generate-reply.validation.ts (1)

1-22: Strong validation approach using Zod
This schema ensures that either textPlain or textHtml is present, which is good. Consider also enforcing minimal length or sanitizing inputs (e.g., removing harmful HTML tags) if there's a risk of injection or excessively large payloads.

apps/web/utils/ai/reply/generate-reply.ts (1)

1-53: Ensure robust error handling
Consider wrapping chatCompletion in a try/catch block to gracefully handle network or AI service errors. Also, confirm that any truncated content beyond 3000 characters doesn't adversely affect reply quality. If necessary, split long emails into multiple chunks for more comprehensive context.

apps/web/utils/ai/reply/generate-nudge.ts (1)

22-27: Enhance system prompt with specific guidelines.

The system prompt could be more specific about:

  • Maximum length of the follow-up email
  • Tone variations based on the number of previous follow-ups
  • Guidelines for maintaining thread context
 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.
+Keep it concise (maximum 4-5 sentences) and friendly. Adjust tone based on the number of previous follow-ups.
 Use context from the previous emails to make it relevant.
+Reference specific points or dates from the previous conversation when appropriate.
 Don't mention that you're an AI.
 Don't reply with a Subject. Only reply with the body of the email.`;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41e6a1e and 061cad9.

📒 Files selected for processing (8)
  • apps/web/app/api/ai/reply/nudge/route.ts (1 hunks)
  • apps/web/components/email-list/EmailThread.tsx (6 hunks)
  • apps/web/utils/actions/generate-reply.ts (1 hunks)
  • apps/web/utils/actions/generate-reply.validation.ts (1 hunks)
  • apps/web/utils/ai/reply/generate-nudge.ts (1 hunks)
  • apps/web/utils/ai/reply/generate-reply.ts (1 hunks)
  • apps/web/utils/auth.ts (1 hunks)
  • apps/web/utils/llms/index.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/utils/auth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/api/ai/reply/nudge/route.ts
🔇 Additional comments (4)
apps/web/components/email-list/EmailThread.tsx (3)

32-33: Use named imports responsibly
Your import usage from "ai/react" and the typed GenerateReplyBody are consistent. Ensure your environment is configured to properly handle AI streaming features, and that the GenerateReplyBody type definition matches the structure required by the backend.


92-119: Confirm plain text drafting scenario
You conditionally set generateNudge to defaultShowReply && !draftReply?.textHtml, which might unintentionally trigger a nudge if there's only plain text in the draft. If you expect drafts with no HTML content but plain text, consider also checking draftReply?.textPlain.


134-134: New 'generateNudge' prop introduced
Extending the EmailMessage props with generateNudge is a clear approach. Ensure that all call sites supply a boolean value or omit it, to prevent undefined behavior when referencing this prop.

Also applies to: 144-144

apps/web/utils/ai/reply/generate-nudge.ts (1)

8-21: Consider adding input validation for messages array.

The function accepts an array of messages but doesn't validate the minimum number of messages required for context.

❌ Verification inconclusive

🏁 Script executed:

#!/bin/bash
# Check if there are any existing validation checks for minimum messages
rg -A 3 "messages.length" apps/web/utils/ai/reply

Length of output: 49


Missing Input Validation for Messages Array

The investigation confirms that there are no checks in place to validate the length of the messages array before proceeding. This means that if the array does not meet the minimum required count for context, the function might operate on unexpected input or even fail.

  • Issue Identified: No validation to ensure the messages array has sufficient entries.
  • Recommendation: Add a guard clause to check that the number of messages meets the minimum requirement. If not, either throw an error or handle the scenario gracefully.

Comment on lines 169 to 199
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]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent repeated AI calls & remove console logs

  1. Triggering complete("") in a useEffect whenever generateNudge is true can cause repeated calls if the prop toggles multiple times. Consider adding a check or controlling repeated invocations if needed.
  2. Remove the console.log("🚀 ~ completion:", completion); statement or replace it with a formal logger before production.

Comment on lines +111 to +149
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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timeout and retry mechanism for transient errors.

The chatCompletion function should handle timeouts and implement retries for transient errors, similar to the existing withRetry utility.

 export async function chatCompletion({
   userAi,
   prompt,
   system,
   userEmail,
   usageLabel,
+  timeout = 30000,
+  maxRetries = 3,
 }: {
   userAi: UserAIFields;
   prompt: string;
   system?: string;
   userEmail: string;
   usageLabel: string;
+  timeout?: number;
+  maxRetries?: number;
 }) {
-  try {
+  return withRetry(
+    async () => {
+      try {
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), timeout);
+
+        try {
           const { provider, model, llmModel } = getModel(userAi);
 
           const result = await generateText({
             model: llmModel,
             prompt,
             system,
             experimental_telemetry: { isEnabled: true },
+            signal: controller.signal,
           });
 
           if (result.usage) {
             await saveAiUsage({
               email: userEmail,
               usage: result.usage,
               provider,
               model,
               label: usageLabel,
             });
           }
 
           return result;
+        } finally {
+          clearTimeout(timeoutId);
+        }
       } catch (error) {
         await handleError(error, userEmail);
         throw error;
       }
-  }
+    },
+    {
+      retryIf: (error) =>
+        error instanceof APICallError || error instanceof RetryError,
+      maxRetries,
+      delayMs: 1000,
+    },
+  );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
}
export async function chatCompletion({
userAi,
prompt,
system,
userEmail,
usageLabel,
timeout = 30000,
maxRetries = 3,
}: {
userAi: UserAIFields;
prompt: string;
system?: string;
userEmail: string;
usageLabel: string;
timeout?: number;
maxRetries?: number;
}) {
return withRetry(
async () => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const { provider, model, llmModel } = getModel(userAi);
const result = await generateText({
model: llmModel,
prompt,
system,
experimental_telemetry: { isEnabled: true },
signal: controller.signal,
});
if (result.usage) {
await saveAiUsage({
email: userEmail,
usage: result.usage,
provider,
model,
label: usageLabel,
});
}
return result;
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
await handleError(error, userEmail);
throw error;
}
},
{
retryIf: (error) =>
error instanceof APICallError || error instanceof RetryError,
maxRetries,
delayMs: 1000,
},
);
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 061cad9 and 396def0.

📒 Files selected for processing (2)
  • apps/web/components/email-list/EmailThread.tsx (5 hunks)
  • apps/web/utils/actions/generate-reply.ts (1 hunks)
🔇 Additional comments (1)
apps/web/components/email-list/EmailThread.tsx (1)

91-118: LGTM!

The generateNudge prop is correctly passed to the EmailMessage component and is properly used to conditionally trigger nudge generation.

Comment on lines 15 to 46
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 };
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor duplicate code into a shared utility function.

Both actions share identical authentication, validation, and message processing logic. Consider extracting the common code into a shared utility function to improve maintainability.

Apply this diff to reduce code duplication:

+async function processEmailRequest({
+  actionType,
+  unsafeData,
+  generator,
+}: {
+  actionType: "nudge" | "reply";
+  unsafeData: GenerateReplySchema;
+  generator: (args: { messages: any[]; user: any }) => Promise<string>;
+}) {
+  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: "",
+    }),
+  }));
+
+  return { user, messages, lastMessage };
+}
+
 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 };
+  async (unsafeData: GenerateReplySchema) => {
+    const result = await processEmailRequest({
+      actionType: "nudge",
+      unsafeData,
+      generator: aiGenerateNudge,
+    });
+    if ("error" in result) return result;
+
+    const { user, messages } = result;
+    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 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),
-      content: emailToContent({
-        textPlain: msg.textPlain,
-        textHtml: msg.textHtml,
-        snippet: "",
-      }),
-    }));
-
-    const text = await aiGenerateReply({ messages, user });
-
-    await saveReply({
-      userId: user.id,
-      messageId: lastMessage.id,
-      reply: text,
-    });
-
-    return { text };
+  async (unsafeData: GenerateReplySchema) => {
+    const result = await processEmailRequest({
+      actionType: "reply",
+      unsafeData,
+      generator: aiGenerateReply,
+    });
+    if ("error" in result) return result;
+
+    const { user, messages, lastMessage } = result;
+
+    const existingReply = await getReply({
+      userId: user.id,
+      messageId: lastMessage.id,
+    });
+    if (existingReply) return { text: existingReply };
+
+    const text = await aiGenerateReply({ messages, user });
+    await saveReply({
+      userId: user.id,
+      messageId: lastMessage.id,
+      reply: text,
+    });
+
+    return { text };
   },
 );

Also applies to: 48-92

Comment on lines 42 to 43
const text = await aiGenerateNudge({ messages, user });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for AI generation calls.

The AI generation calls (aiGenerateNudge and aiGenerateReply) should be wrapped in try-catch blocks to handle potential failures gracefully.

Apply this diff to add error handling:

-    const text = await aiGenerateNudge({ messages, user });
-    return { text };
+    try {
+      const text = await aiGenerateNudge({ messages, user });
+      return { text };
+    } catch (error) {
+      console.error("Failed to generate nudge:", error);
+      return { error: "Failed to generate nudge. Please try again." };
+    }

-    const text = await aiGenerateReply({ messages, user });
+    try {
+      const text = await aiGenerateReply({ messages, user });
+    } catch (error) {
+      console.error("Failed to generate reply:", error);
+      return { error: "Failed to generate reply. Please try again." };
+    }

Also applies to: 82-83

Comment on lines 168 to 188
useEffect(() => {
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);
}

if (generateNudge) loadNudge();
}, [generateNudge, message]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove console.log and add error handling.

The implementation needs the following improvements:

  1. Remove the console.log statement.
  2. Add error handling for the nudge generation call.

Apply this diff to improve the implementation:

   useEffect(() => {
     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,
-          },
-        ],
-      });
+      try {
+        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);
+        if (result.error) {
+          // Handle error (e.g., show toast notification)
+          console.error("Failed to generate nudge:", result.error);
+        }
+      } catch (error) {
+        // Handle error (e.g., show toast notification)
+        console.error("Failed to generate nudge:", error);
+      }
     }

     if (generateNudge) loadNudge();
   }, [generateNudge, message]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
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);
}
if (generateNudge) loadNudge();
}, [generateNudge, message]);
useEffect(() => {
async function loadNudge() {
try {
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,
},
],
});
if (result.error) {
// Handle error (e.g., show toast notification)
console.error("Failed to generate nudge:", result.error);
}
} catch (error) {
// Handle error (e.g., show toast notification)
console.error("Failed to generate nudge:", error);
}
}
if (generateNudge) loadNudge();
}, [generateNudge, message]);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
apps/web/components/email-list/EmailDetails.tsx (1)

9-12: Consider improving date handling.

The current date formatting might not handle invalid dates gracefully and doesn't consider user's timezone preferences.

Apply this diff to improve date handling:

-      value: new Date(message.headers.date).toLocaleString(),
+      value: message.headers.date
+        ? new Date(message.headers.date).toLocaleString(undefined, {
+            dateStyle: "long",
+            timeStyle: "short",
+          })
+        : "No date",
apps/web/components/email-list/EmailContents.tsx (1)

43-67: Consider improving HTML injection safety.

The getIframeHtml function should handle malformed HTML more safely.

Consider using a proper HTML parser:

 function getIframeHtml(html: string) {
   const defaultFontStyles = `
     <style>
       /* Base styles with low specificity */
       body {
         font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
         margin: 0;
       }
     </style>
   `;
 
+  const parser = new DOMParser();
+  const doc = parser.parseFromString(html, "text/html");
+  const hasHead = doc.head !== null;
+
   let htmlWithHead = "";
-  if (html.indexOf("</head>") === -1) {
+  if (!hasHead) {
     htmlWithHead = `<head>${defaultFontStyles}<base target="_blank"></head>${html}`;
   } else {
-    htmlWithHead = html.replace(
-      "</head>",
-      `${defaultFontStyles}<base target="_blank" rel="noopener noreferrer"></head>`,
-    );
+    doc.head.insertAdjacentHTML("beforeend", defaultFontStyles);
+    doc.head.insertAdjacentHTML("beforeend", '<base target="_blank" rel="noopener noreferrer">');
+    htmlWithHead = doc.documentElement.outerHTML;
   }
 
   return htmlWithHead;
 }
apps/web/components/email-list/EmailMessage.tsx (2)

51-59: Consider using a more robust scroll implementation.

The current implementation uses a setTimeout which is noted as "hacky". A more reliable approach would be to use a callback after the component is mounted and rendered.

Consider this alternative implementation:

-  useEffect(() => {
-    if (defaultShowReply && replyRef.current) {
-      setTimeout(() => {
-        replyRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
-        // NOTE: a little hacky
-        // If this is set lower it doesn't work (or if we turn off autofocus, it does, but we want autofocus).
-      }, 500);
-    }
-  }, [defaultShowReply]);
+  const scrollToReply = useCallback(() => {
+    if (replyRef.current) {
+      replyRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
+    }
+  }, []);
+
+  useEffect(() => {
+    if (defaultShowReply) {
+      // Use requestAnimationFrame to ensure DOM is updated
+      requestAnimationFrame(scrollToReply);
+    }
+  }, [defaultShowReply, scrollToReply]);

106-107: Add explanation for the biome-ignore comment.

The biome-ignore comment for a11y/useKeyWithClickEvents should include an explanation of why the rule is being ignored.

-// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
+// biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard events are handled by the parent component
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 396def0 and 7238346.

📒 Files selected for processing (8)
  • apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx (1 hunks)
  • apps/web/components/email-list/EmailAttachments.tsx (1 hunks)
  • apps/web/components/email-list/EmailContents.tsx (1 hunks)
  • apps/web/components/email-list/EmailDetails.tsx (1 hunks)
  • apps/web/components/email-list/EmailMessage.tsx (1 hunks)
  • apps/web/components/email-list/EmailThread.tsx (3 hunks)
  • apps/web/components/email-list/types.ts (1 hunks)
  • apps/web/utils/actions/generate-reply.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/components/email-list/EmailThread.tsx
  • apps/web/utils/actions/generate-reply.ts
🔇 Additional comments (4)
apps/web/components/email-list/types.ts (1)

15-15: LGTM! Good type extraction pattern.

The new ThreadMessage type is well-defined and improves code readability by extracting a specific type from the Thread type.

apps/web/components/email-list/EmailContents.tsx (1)

39-41: LGTM! Simple and effective implementation.

The PlainEmail component is well-implemented with proper whitespace handling.

apps/web/components/email-list/EmailMessage.tsx (1)

1-46: LGTM! Well-structured component props and imports.

The component's props are well-typed and provide clear functionality for managing email message display and interaction.

apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx (1)

174-216: LGTM! Good handling of panel overflow.

The implementation of independent scrolling for both panels using h-full overflow-y-auto is a good practice. This ensures that each panel can scroll its content independently while maintaining the overall layout structure.

Comment on lines 153 to 158
<Tooltip content="Forward">
<Button variant="ghost" size="icon">
<ForwardIcon className="h-4 w-4" onClick={onForward} />
<span className="sr-only">Forward</span>
</Button>
</Tooltip>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix click handler placement on Forward button.

The onClick handler is incorrectly placed on the ForwardIcon instead of the Button component.

-              <Tooltip content="Forward">
-                <Button variant="ghost" size="icon">
-                  <ForwardIcon className="h-4 w-4" onClick={onForward} />
-                  <span className="sr-only">Forward</span>
-                </Button>
-              </Tooltip>
+              <Tooltip content="Forward">
+                <Button variant="ghost" size="icon" onClick={onForward}>
+                  <ForwardIcon className="h-4 w-4" />
+                  <span className="sr-only">Forward</span>
+                </Button>
+              </Tooltip>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tooltip content="Forward">
<Button variant="ghost" size="icon">
<ForwardIcon className="h-4 w-4" onClick={onForward} />
<span className="sr-only">Forward</span>
</Button>
</Tooltip>
<Tooltip content="Forward">
<Button variant="ghost" size="icon" onClick={onForward}>
<ForwardIcon className="h-4 w-4" />
<span className="sr-only">Forward</span>
</Button>
</Tooltip>

Comment on lines 70 to 90
useEffect(() => {
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);
}

if (generateNudge) loadNudge();
}, [generateNudge, message]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove console.log and add error handling.

The nudge generation implementation needs two improvements:

  1. Remove the debug console.log statement
  2. Add error handling for the generateNudgeAction call

Consider this improved implementation:

 useEffect(() => {
   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);
+    try {
+      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,
+          },
+        ],
+      });
+      // Handle successful result
+    } catch (error) {
+      // Handle error appropriately
+      console.error('Failed to generate nudge:', error);
+    }
   }

   if (generateNudge) loadNudge();
 }, [generateNudge, message]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
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);
}
if (generateNudge) loadNudge();
}, [generateNudge, message]);
useEffect(() => {
async function loadNudge() {
try {
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,
},
],
});
// Handle successful result
} catch (error) {
// Handle error appropriately
console.error('Failed to generate nudge:', error);
}
}
if (generateNudge) loadNudge();
}, [generateNudge, message]);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/web/utils/actions/generate-reply.validation.ts (2)

3-15: Enhance schema validation rules for better data integrity.

Consider adding these validations:

  1. Email format validation for from and to fields
  2. Date format validation for the date field
  3. Basic HTML content validation for textHtml
 const messageSchema = z
   .object({
     id: z.string(),
-    from: z.string(),
-    to: z.string(),
+    from: z.string().email(),
+    to: z.string().email(),
     subject: z.string(),
     textPlain: z.string().optional(),
-    textHtml: z.string().optional(),
+    textHtml: z.string()
+      .regex(/<[^>]*>/)
+      .optional(),
-    date: z.string(),
+    date: z.string()
+      .datetime()
+      .or(z.date()),
   })

Also, consider adding JSDoc comments to document the schema fields:

/** Message schema for email reply generation
 * @property {string} id - Unique identifier for the message
 * @property {string} from - Sender's email address
 * @property {string} to - Recipient's email address
 * @property {string} subject - Email subject
 * @property {string} [textPlain] - Plain text content
 * @property {string} [textHtml] - HTML content
 * @property {string} date - Message date
 */

17-20: Add array length validation and documentation.

Consider adding minimum length validation for the messages array and JSDoc comments.

+/** Schema for generating email replies and nudges
+ * @property {("reply"|"nudge")} type - Type of generation
+ * @property {Message[]} messages - Array of messages to process
+ */
 export const generateReplySchema = z.object({
   type: z.enum(["reply", "nudge"]),
-  messages: z.array(messageSchema),
+  messages: z.array(messageSchema).min(1, "At least one message is required"),
 });
apps/web/components/email-list/EmailMessage.tsx (1)

210-217: Replace setTimeout with a more reliable solution.

Using setTimeout for scroll behavior is fragile and may not work consistently across different devices and load times.

Consider using a MutationObserver or ResizeObserver for more reliable scroll behavior:

-  useEffect(() => {
-    if (defaultShowReply && replyRef.current) {
-      // hacky using setTimeout
-      setTimeout(() => {
-        replyRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
-      }, 500);
-    }
-  }, [defaultShowReply]);
+  useEffect(() => {
+    if (defaultShowReply && replyRef.current) {
+      const observer = new ResizeObserver(() => {
+        replyRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
+      });
+      observer.observe(replyRef.current);
+      return () => observer.disconnect();
+    }
+  }, [defaultShowReply]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7238346 and 98b0c48.

📒 Files selected for processing (5)
  • apps/web/components/email-list/EmailMessage.tsx (1 hunks)
  • apps/web/utils/actions/generate-reply.ts (1 hunks)
  • apps/web/utils/actions/generate-reply.validation.ts (1 hunks)
  • apps/web/utils/ai/reply/generate-nudge.ts (1 hunks)
  • apps/web/utils/ai/reply/generate-reply.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/utils/ai/reply/generate-nudge.ts
  • apps/web/utils/actions/generate-reply.ts
  • apps/web/utils/ai/reply/generate-reply.ts
🔇 Additional comments (2)
apps/web/utils/actions/generate-reply.validation.ts (1)

22-22: LGTM!

The type export is well-defined and follows TypeScript best practices by inferring the type from the Zod schema.

apps/web/components/email-list/EmailMessage.tsx (1)

174-177: Fix click handler placement on Forward button.

The onClick handler is incorrectly placed on the ForwardIcon instead of the Button component.

@elie222 elie222 changed the title WIP: AI generate reply / nudge endpoints AI generate reply / nudge endpoints Feb 10, 2025
@elie222 elie222 merged commit fbed4af into main Feb 10, 2025
1 of 4 checks passed
@elie222 elie222 deleted the generate-reply branch February 10, 2025 16:04
@coderabbitai coderabbitai bot mentioned this pull request Sep 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments