Skip to content

Smart Categories#252

Merged
elie222 merged 81 commits intomainfrom
categorise-senders
Nov 1, 2024
Merged

Smart Categories#252
elie222 merged 81 commits intomainfrom
categorise-senders

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Nov 1, 2024

Categorize senders using AI.
Bulk archive by category and use categories as part of AI rule filters.

Summary by CodeRabbit

  • New Features

    • Introduced a new CategorizeWithAiButton for categorizing senders using AI.
    • Added CreateCategoryButton for creating categories with a modal interface.
    • Implemented Uncategorized component for managing uncategorized senders.
    • Launched KanbanBoard and TaskCard components for a kanban board interface.
    • Added useCategories hook for fetching user categories.
    • Introduced TopBar component for improved layout.
  • Bug Fixes

    • Enhanced error handling and state management in various components and hooks.
  • Improvements

    • Updated authentication checks for session validation across API routes.
    • Improved UI components with new hooks and better state management.
    • Enhanced categorization logic to include category filters for rules.
  • Refactor

    • Streamlined email categorization logic and state management in multiple files.
    • Refactored EmailList and EmailPanel components for better performance and clarity.
    • Simplified the Modal component structure and prop usage.
  • Documentation

    • Updated documentation to reflect new features and changes in functionality.

@vercel
Copy link

vercel bot commented Nov 1, 2024

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

Name Status Preview Updated (UTC)
inbox-zero ✅ Ready (Inspect) Visit Preview Nov 1, 2024 5:44am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 1, 2024

Caution

Review failed

The pull request is closed.

Walkthrough

The pull request introduces several changes across multiple files, focusing on enhancing error handling, refining API route logic, and improving user session validation. Key modifications include updating authentication checks in API snippets, introducing new components for categorizing senders, and enhancing the management of email archiving and categorization queues. Additionally, new hooks and utility functions have been added to streamline data fetching and state management, particularly for user categories and AI-related functionalities.

Changes

File Path Change Summary
.vscode/typescriptreact.code-snippets Updated authentication checks in API route snippets to validate session?.user.
apps/web/.env.example Changed BEDROCK_REGION from us-east-1 to us-west-2.
apps/web/__tests__/ai-categorize-senders.test.ts Introduced unit tests for the aiCategorizeSenders function with various scenarios.
apps/web/app/(app)/automation/BulkRunRules.tsx Replaced useSWR with useThreads for data fetching; updated state management with useAiQueueState.
apps/web/app/(app)/automation/RuleForm.tsx Added CategoryFilterType import; enhanced form with category filtering functionality.
apps/web/app/(app)/automation/RulesPrompt.tsx Adjusted error handling logic in RulesPromptForm component.
apps/web/app/(app)/automation/group/CreateGroupModal.tsx Integrated Zod validation with React Hook Form using zodResolver.
apps/web/app/(app)/bulk-unsubscribe/ArchiveProgress.tsx Updated state management from useAtomValue to useQueueState.
apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribe.tsx Replaced custom div with TopBar component for layout.
apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeMobile.tsx Refined button loading states; removed unused imports.
apps/web/app/(app)/bulk-unsubscribe/hooks.ts Refactored email archiving and deletion functions; updated promise handling.
apps/web/app/(app)/compose/selectors/color-selector.tsx Updated ColorSelectorProps interface for open state management.
apps/web/app/(app)/onboarding/OnboardingBulkUnsubscriber.tsx Replaced email rendering with EmailCell component.
apps/web/app/(app)/premium/config.ts Updated lifetimeFeatures and added ctaLink to copilotTier.
apps/web/app/(app)/request-access/EnableEarlyAccess.tsx Introduced new EnableEarlyAccess component.
apps/web/app/(app)/request-access/page.tsx Changed page content to focus on enabling early access features.
apps/web/app/(app)/simple/SimpleList.tsx Updated archiveEmails import and invocation.
apps/web/app/(app)/simple/SimpleModeOnboarding.tsx Refactored onboarding modal logic using useOnboarding hook.
apps/web/app/(app)/smart-categories/CategorizeWithAiButton.tsx Introduced button for categorizing senders using AI.
apps/web/app/(app)/smart-categories/CreateCategoryButton.tsx Added modal interface for creating categories with validation.
apps/web/app/(app)/smart-categories/Uncategorized.tsx Implemented infinite scrolling for uncategorized senders.
apps/web/app/(app)/smart-categories/board/page.tsx Created Kanban board for user-specific categories.
apps/web/app/(app)/smart-categories/page.tsx Managed smart categories with dynamic rendering based on user authentication.
apps/web/app/(app)/smart-categories/setup/SetUpCategories.tsx Introduced category setup functionality with CRUD operations.
apps/web/app/api/ai/categorize/controller.ts Renamed functions for consistency; updated type exports.
apps/web/app/api/ai/categorize/validation.ts Renamed categorization-related objects and types.
apps/web/app/api/ai/cold-email/route.ts Updated cold email checking logic to focus on senders.
apps/web/app/api/google/messages/route.ts Enhanced session validation logic for message retrieval.
apps/web/app/api/google/threads/controller.ts Introduced helper function for query string construction.
apps/web/app/api/google/watch/all/route.ts Modified user access checks for AI features.
apps/web/app/api/google/webhook/process-history.ts Updated rule processing logic to include category filters.
apps/web/app/api/user/categories/route.ts Introduced API endpoint for retrieving user categories.
apps/web/app/api/user/categorize/senders/find-senders.ts Added functions for paginated retrieval of email senders.
apps/web/app/api/user/categorize/senders/types.ts Defined SenderMap type for structured sender management.
apps/web/app/api/user/categorize/senders/uncategorized/route.ts Created endpoint for fetching uncategorized senders.
apps/web/app/api/user/complete-registration/route.ts Updated CompleteRegistrationBody type for clarity.
apps/web/components/CommandK.tsx Updated navigation handling and archiving functionality.
apps/web/components/EmailCell.tsx Introduced new component for displaying email addresses.
apps/web/components/GroupedTable.tsx Added component for displaying grouped email data in a table format.
apps/web/components/Modal.tsx Streamlined modal props and structure.
apps/web/components/MultiSelectFilter.tsx Introduced multi-select filtering component with hooks.
apps/web/components/OnboardingModal.tsx Added custom hook for managing onboarding state.
apps/web/components/PremiumAlert.tsx Updated plan prop and description text.
apps/web/components/Select.tsx Added tooltipText property to Select component.
apps/web/components/SideNav.tsx Updated navigation items and filtering logic for smart categories.
apps/web/components/TooltipExplanation.tsx Enhanced tooltip component with size variants.
apps/web/components/TopBar.tsx Introduced new TopBar component for layout.
apps/web/components/email-list/EmailList.tsx Enhanced email handling logic with improved error management.
apps/web/components/email-list/EmailListItem.tsx Simplified AI queue status checking logic.
apps/web/components/email-list/EmailPanel.tsx Streamlined AI queue status determination.
apps/web/components/kanban/BoardColumn.tsx Introduced draggable column component for kanban board.
apps/web/components/kanban/KanbanBoard.tsx Added main Kanban board component with drag-and-drop functionality.
apps/web/components/kanban/TaskCard.tsx Created task card component for kanban interface.
apps/web/components/kanban/kanban-utils.ts Added utility function for draggable data validation.
apps/web/components/kanban/multipleContainersKeyboardPreset.ts Introduced keyboard navigation support for droppable containers.
apps/web/components/ui/button.tsx Updated button variants for new size options.
apps/web/hooks/useCategories.ts Introduced custom hook for fetching user categories.
apps/web/hooks/useFeatureFlags.ts Added hooks for managing feature flags.
apps/web/hooks/useThreads.ts Created hook for fetching thread data with query parameters.
apps/web/next.config.mjs Enhanced security headers in Next.js configuration.
apps/web/package.json Simplified dev script and added new dependencies for drag-and-drop functionality.
apps/web/prisma/migrations/20241023204900_category/migration.sql Created new Category table and modified existing tables.
apps/web/prisma/migrations/20241027173153_category_filter/migration.sql Introduced CategoryFilterType enum and modified Rule table.
apps/web/prisma/migrations/20241031212440_auto_categorize_senders/migration.sql Added autoCategorizeSenders column to User table.
apps/web/prisma/schema.prisma Updated models and added new fields for categories and rules.
apps/web/providers/PostHogProvider.tsx Added opt_in_site_apps option to PostHog initialization.
apps/web/store/QueueInitializer.tsx Updated queue state access method.
apps/web/store/ai-categorize-sender-queue.ts Introduced AI sender categorization queue management.
apps/web/store/ai-queue.ts Established AI queue state management system.
apps/web/store/archive-queue.ts Enhanced archive queue functionality with improved error handling.
apps/web/store/archive-sender-queue.ts Added functionality for managing an archive sender queue.
apps/web/store/queue.ts Deleted obsolete queue management file.
apps/web/utils/actions/ai-rule.ts Enhanced rule actions to include category filters.
apps/web/utils/actions/categorize.ts Updated categorization logic and introduced new functions for sender categorization.
apps/web/utils/actions/helpers.ts Modified helper function to include full session object in the return value.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant API
    participant Database

    User->>Frontend: Request to categorize sender
    Frontend->>API: Send categorize request
    API->>Database: Fetch user session
    Database-->>API: Return session data
    API->>Database: Fetch sender data
    Database-->>API: Return sender data
    API->>Frontend: Return categorization result
    Frontend->>User: Display categorization result
Loading

Possibly related PRs

Poem

🐇 In the meadow where the code does flow,
New features sprout, like flowers that grow.
With each change made, the logic's refined,
A garden of code, beautifully aligned.
So hop along, dear coder, with glee,
For every pull request brings joy, you see! 🌼


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 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.

@elie222 elie222 merged commit 3edb923 into main Nov 1, 2024
@elie222 elie222 deleted the categorise-senders branch November 1, 2024 05:39
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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

🛑 Comments failed to post (96)
apps/web/app/(app)/request-access/EnableEarlyAccess.tsx (1)

3-5: 🛠️ Refactor suggestion

Add essential functionality to the button.

The component structure looks good, but it's missing critical functionality:

  1. Click handler to process the early access enablement
  2. Loading state during processing
  3. Error handling and user feedback
  4. Type safety with TypeScript

Consider implementing these improvements:

+ import { useState } from "react";
  import { Button } from "@/components/Button";
+ import { toast } from "sonner";
+ 
+ interface EnableEarlyAccessProps {
+   onSuccess?: () => void;
+ }
+ 
- export function EnableEarlyAccess() {
+ export function EnableEarlyAccess({ onSuccess }: EnableEarlyAccessProps) {
+   const [isLoading, setIsLoading] = useState(false);
+ 
+   const handleEnableAccess = async () => {
+     try {
+       setIsLoading(true);
+       // TODO: Add API call to enable early access
+       toast.success("Early access features enabled!");
+       onSuccess?.();
+     } catch (error) {
+       toast.error("Failed to enable early access features");
+     } finally {
+       setIsLoading(false);
+     }
+   };
+ 
-   return <Button id="beta-button">Enable Early Access Features</Button>;
+   return (
+     <Button
+       id="beta-button"
+       onClick={handleEnableAccess}
+       loading={isLoading}
+       disabled={isLoading}
+     >
+       Enable Early Access Features
+     </Button>
+   );
  }

Committable suggestion skipped: line range outside the PR's diff.

apps/web/hooks/useCategories.ts (1)

4-10: 🛠️ Refactor suggestion

Consider enhancing the hook with SWR configuration options.

While the implementation is functional, it could benefit from additional configuration for better performance and reliability:

  1. Add revalidation options
  2. Include error retry configuration
  3. Improve type safety for the fallback array

Consider applying these improvements:

 export function useCategories() {
   const { data, isLoading, error, mutate } = useSWR<UserCategoriesResponse>(
     "/api/user/categories",
+    {
+      revalidateOnFocus: false,
+      revalidateOnReconnect: true,
+      retry: 3,
+      dedupingInterval: 60000, // 1 minute
+    }
   );

-  return { categories: data?.result || [], isLoading, error, mutate };
+  return {
+    categories: data?.result ?? ([] as UserCategoriesResponse['result']),
+    isLoading,
+    error,
+    mutate
+  };
 }

This enhancement:

  • Reduces unnecessary revalidation on window focus
  • Adds retry logic for failed requests
  • Implements proper type assertion for the fallback array
  • Adds deduping to prevent excessive requests
📝 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.

export function useCategories() {
  const { data, isLoading, error, mutate } = useSWR<UserCategoriesResponse>(
    "/api/user/categories",
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
      retry: 3,
      dedupingInterval: 60000, // 1 minute
    }
  );

  return {
    categories: data?.result ?? ([] as UserCategoriesResponse['result']),
    isLoading,
    error,
    mutate
  };
}
apps/web/hooks/useFeatureFlags.ts (1)

10-16: 🛠️ Refactor suggestion

Add type safety for variant keys and document possible values.

Both hooks could benefit from improved type safety and documentation of possible variant values.

+type LandingPageVariant = "control" | "variant_a" | "variant_b" | null;
+type AppOnboardingVariant = "simple" | "detailed" | null;
+
-export function useLandingPageVariant() {
+export function useLandingPageVariant(): LandingPageVariant {
   return useFeatureFlagVariantKey("landing-page-features");
 }

-export function useAppOnboardingVariant() {
+export function useAppOnboardingVariant(): AppOnboardingVariant {
   return useFeatureFlagVariantKey("app-onboarding");
 }

Consider adding JSDoc comments to document the purpose and possible return values of each hook:

/**
 * Hook to get the current landing page variant.
 * @returns The variant key: 'control', 'variant_a', 'variant_b', or null if the feature flag is not set
 */

/**
 * Hook to get the current app onboarding variant.
 * @returns The variant key: 'simple', 'detailed', or null if the feature flag is not set
 */
apps/web/components/TopBar.tsx (2)

3-11: 🛠️ Refactor suggestion

Consider enhancing type safety and documentation

While the type definitions are good, we can improve the component's maintainability and reusability.

Consider these enhancements:

+import { type HTMLAttributes } from "react";
+
+interface TopBarProps extends HTMLAttributes<HTMLDivElement> {
+  /** The content to be rendered inside the top bar */
+  children: React.ReactNode;
+  /** Whether the top bar should stick to the top of the viewport */
+  sticky?: boolean;
+  /** Additional CSS classes to be applied to the top bar */
+  className?: string;
+}
+
-export function TopBar({
+export function TopBar({
   children,
   sticky = false,
   className,
+  ...props
-}: {
-  children: React.ReactNode;
-  sticky?: boolean;
-  className?: string;
-}) {
+}: TopBarProps) {

This change:

  1. Extracts the interface for better reusability
  2. Adds JSDoc comments for better documentation
  3. Extends HTMLAttributes for complete div props support
📝 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.

import { type HTMLAttributes } from "react";

interface TopBarProps extends HTMLAttributes<HTMLDivElement> {
  /** The content to be rendered inside the top bar */
  children: React.ReactNode;
  /** Whether the top bar should stick to the top of the viewport */
  sticky?: boolean;
  /** Additional CSS classes to be applied to the top bar */
  className?: string;
}

export function TopBar({
  children,
  sticky = false,
  className,
  ...props
}: TopBarProps) {

12-22: 🛠️ Refactor suggestion

Consider accessibility improvements

The component implementation is clean, but we can enhance its accessibility.

Consider these improvements:

   return (
     <div
+      role="banner"
+      {...props}
       className={cn(
         "flex flex-col justify-between gap-1 border-b bg-white px-2 py-2 shadow sm:flex-row sm:px-4",
         sticky && "top-0 z-10 sm:sticky",
         className,
       )}
     >
       {children}
     </div>
   );

The role="banner" attribute helps screen readers identify this as a landmark region, typically used for headers and top bars.

Committable suggestion skipped: line range outside the PR's diff.

apps/web/store/QueueInitializer.tsx (1)

7-7: ⚠️ Potential issue

Consider replacing module-level variable with React state

Using a module-level variable for tracking initialization state could lead to issues with concurrent rendering and React's strict mode. This approach isn't safe for server components either.

Consider refactoring to use React's useState:

-let isInitialized = false;
 
 function useInitializeQueues() {
+  const [isInitialized, setIsInitialized] = useState(false);
   const queueState = useQueueState();
 
   useEffect(() => {
-    if (!isInitialized) {
-      isInitialized = true;
+    if (!isInitialized) {
+      setIsInitialized(true);

Committable suggestion skipped: line range outside the PR's diff.

apps/web/components/EmailCell.tsx (3)

1-9: 🛠️ Refactor suggestion

Consider extracting props interface.

While the implementation is solid, consider extracting the props interface for better reusability and maintainability.

+interface EmailCellProps {
+  emailAddress: string;
+  className?: string;
+}

-export const EmailCell = memo(function EmailCell({
+export const EmailCell = memo(function EmailCell({
   emailAddress,
   className,
-}: {
-  emailAddress: string;
-  className?: string;
-})
+}: EmailCellProps)
📝 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.

import { memo } from "react";

interface EmailCellProps {
  emailAddress: string;
  className?: string;
}

export const EmailCell = memo(function EmailCell({
  emailAddress,
  className,
}: EmailCellProps) {

17-22: 🛠️ Refactor suggestion

Enhance accessibility and UI robustness.

The rendering structure could benefit from improved accessibility and handling of long content.

Consider these improvements:

   return (
-    <div className={className}>
+    <div className={`${className} overflow-hidden`} role="cell">
-      <div>{name}</div>
+      <div className="truncate" title={name}>{name}</div>
-      <div className="text-slate-500">{email}</div>
+      <div className="truncate text-slate-500" title={email}>{email}</div>
     </div>
   );
📝 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.

  return (
    <div className={`${className} overflow-hidden`} role="cell">
      <div className="truncate" title={name}>{name}</div>
      <div className="truncate text-slate-500" title={email}>{email}</div>
    </div>
  );

10-15: 🛠️ Refactor suggestion

Enhance email parsing robustness and efficiency.

The current implementation has several potential improvements:

  1. Email parsing is performed twice
  2. The regex pattern might miss edge cases
  3. No validation of email format

Consider this more robust implementation:

-  const parseEmail = (name: string) => {
-    const match = name.match(/<(.+)>/);
-    return match ? match[1] : name;
-  };
-  const name = emailAddress.split("<")[0].trim();
-  const email = parseEmail(emailAddress);
+  const parseEmailAndName = (input: string) => {
+    const match = input.match(/^([^<]*?)\s*(?:<(.+?)>)?$/);
+    if (!match) return { name: input, email: input };
+    const [, name, email] = match;
+    return {
+      name: name.trim() || email,
+      email: email || name.trim(),
+    };
+  };
+  const { name, email } = parseEmailAndName(emailAddress);
📝 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.

  const parseEmailAndName = (input: string) => {
    const match = input.match(/^([^<]*?)\s*(?:<(.+?)>)?$/);
    if (!match) return { name: input, email: input };
    const [, name, email] = match;
    return {
      name: name.trim() || email,
      email: email || name.trim(),
    };
  };
  const { name, email } = parseEmailAndName(emailAddress);
apps/web/app/api/user/categories/route.ts (2)

8-11: 🛠️ Refactor suggestion

Consider adding input validation and simplifying the return structure.

The function could benefit from:

  1. Input validation for the userId parameter
  2. A simpler return structure without unnecessary nesting

Consider this implementation:

 async function getCategories({ userId }: { userId: string }) {
+  if (!userId) throw new Error("userId is required");
-  const result = await getUserCategories(userId);
-  return { result };
+  return getUserCategories(userId);
 }
📝 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.

async function getCategories({ userId }: { userId: string }) {
  if (!userId) throw new Error("userId is required");
  return getUserCategories(userId);
}

15-16: 🛠️ Refactor suggestion

Enhance error response structure.

Consider using a consistent error response structure that includes a status code and more detailed message.

-    return NextResponse.json({ error: "Not authenticated" });
+    return NextResponse.json(
+      { 
+        error: "Authentication required",
+        code: "UNAUTHORIZED",
+        message: "You must be logged in to access categories"
+      },
+      { status: 401 }
+    );
📝 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.

  if (!session?.user.id)
    return NextResponse.json(
      { 
        error: "Authentication required",
        code: "UNAUTHORIZED",
        message: "You must be logged in to access categories"
      },
      { status: 401 }
    );
apps/web/hooks/useThreads.ts (1)

21-23: 🛠️ Refactor suggestion

Consider adding error handling and data transformation

The hook could benefit from error handling and data transformation logic to provide a more robust API.

  const { data, isLoading, error, mutate } = useSWR<ThreadsResponse>(url, {
    refreshInterval,
+   onError: (err) => {
+     console.error('Error fetching threads:', err);
+   },
+   // Transform the response data if needed
+   transform: (data: ThreadsResponse) => {
+     return {
+       ...data,
+       threads: data.threads?.map(thread => ({
+         ...thread,
+         // Add any computed properties here
+       }))
+     };
+   }
  });

Committable suggestion skipped: line range outside the PR's diff.

apps/web/prisma/migrations/20241023204900_category/migration.sql (2)

1-2: 🛠️ Refactor suggestion

Consider adding an index on categoryId column.

The categoryId column will likely be used in JOIN operations and WHERE clauses when filtering newsletters by category. Adding an index would improve query performance.

CREATE INDEX "Newsletter_categoryId_idx" ON "Newsletter"("categoryId");

4-14: ⚠️ Potential issue

Make userId NOT NULL and consider adding soft delete.

Several concerns with the Category table design:

  1. userId should be NOT NULL as categories should always belong to a user
  2. Consider adding a soft delete column (e.g., deletedAt) for safer deletion handling

Apply these modifications:

 CREATE TABLE "Category" (
     "id" TEXT NOT NULL,
     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "updatedAt" TIMESTAMP(3) NOT NULL,
     "name" TEXT NOT NULL,
     "description" TEXT,
-    "userId" TEXT,
+    "userId" TEXT NOT NULL,
+    "deletedAt" TIMESTAMP(3),

     CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
 );
📝 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.

-- CreateTable
CREATE TABLE "Category" (
    "id" TEXT NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,
    "name" TEXT NOT NULL,
    "description" TEXT,
    "userId" TEXT NOT NULL,
    "deletedAt" TIMESTAMP(3),

    CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
apps/web/app/(app)/smart-categories/setup/page.tsx (2)

13-13: ⚠️ Potential issue

Add error handling for category fetching.

The getUserCategories call should include error handling to gracefully handle potential failures.

-  const categories = await getUserCategories(session.user.id);
+  try {
+    const categories = await getUserCategories(session.user.id);
+  } catch (error) {
+    console.error("Failed to fetch user categories:", error);
+    throw new Error("Unable to load categories. Please try again later.");
+  }

Committable suggestion skipped: line range outside the PR's diff.


8-11: 🛠️ Refactor suggestion

Enhance authentication error handling and type safety.

The current authentication check could be improved in several ways:

  1. The error message could be more descriptive for debugging
  2. Consider using a more graceful error handling approach instead of throwing
 export default async function SetupCategoriesPage() {
   const session = await auth();
+  if (!session) {
+    throw new Error("Authentication required to access smart categories setup");
+  }
+
   const email = session?.user.email;
-  if (!email) throw new Error("Not authenticated");
+  if (!email) {
+    throw new Error("User email not found in session");
+  }
📝 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.

export default async function SetupCategoriesPage() {
  const session = await auth();
  if (!session) {
    throw new Error("Authentication required to access smart categories setup");
  }

  const email = session?.user.email;
  if (!email) {
    throw new Error("User email not found in session");
  }
apps/web/store/ai-queue.ts (2)

21-27: 🛠️ Refactor suggestion

Add validation and handle non-existent IDs.

Consider adding input validation and handling cases where the ID doesn't exist in the queue:

 export const removeFromAiQueueAtom = (removeId: string) => {
+  if (typeof removeId !== 'string' || !removeId) {
+    console.warn('Invalid ID provided to removeFromAiQueueAtom');
+    return;
+  }
   jotaiStore.set(aiQueueAtom, (prev) => {
+    if (!prev.has(removeId)) {
+      console.warn(`ID ${removeId} not found in queue`);
+      return prev;
+    }
     const remainingIds = new Set(prev);
     remainingIds.delete(removeId);
     return remainingIds;
📝 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.

export const removeFromAiQueueAtom = (removeId: string) => {
  if (typeof removeId !== 'string' || !removeId) {
    console.warn('Invalid ID provided to removeFromAiQueueAtom');
    return;
  }
  jotaiStore.set(aiQueueAtom, (prev) => {
    if (!prev.has(removeId)) {
      console.warn(`ID ${removeId} not found in queue`);
      return prev;
    }
    const remainingIds = new Set(prev);
    remainingIds.delete(removeId);
    return remainingIds;
  });
};

11-19: 🛠️ Refactor suggestion

Add input validation and error handling.

While the implementation is functionally correct, consider adding input validation and error handling:

 export const pushToAiQueueAtom = (pushIds: string[]) => {
+  if (!Array.isArray(pushIds) || pushIds.length === 0) {
+    console.warn('Invalid or empty pushIds array provided to pushToAiQueueAtom');
+    return;
+  }
+  if (pushIds.some(id => typeof id !== 'string' || !id)) {
+    console.warn('Invalid ID found in pushIds array');
+    return;
+  }
   jotaiStore.set(aiQueueAtom, (prev) => {
     const newIds = new Set(prev);
     for (const id of pushIds) {
📝 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.

export const pushToAiQueueAtom = (pushIds: string[]) => {
  if (!Array.isArray(pushIds) || pushIds.length === 0) {
    console.warn('Invalid or empty pushIds array provided to pushToAiQueueAtom');
    return;
  }
  if (pushIds.some(id => typeof id !== 'string' || !id)) {
    console.warn('Invalid ID found in pushIds array');
    return;
  }
  jotaiStore.set(aiQueueAtom, (prev) => {
    const newIds = new Set(prev);
    for (const id of pushIds) {
      newIds.add(id);
    }
    return newIds;
  });
};
apps/web/components/Select.tsx (1)

20-26: 🛠️ Refactor suggestion

Consider simplifying the conditional rendering

While the current implementation is correct, the conditional rendering could be more concise.

Consider this more idiomatic React approach:

-      {props.label ? (
-        <Label
-          name={props.name}
-          label={props.label}
-          tooltipText={props.tooltipText}
-        />
-      ) : null}
+      {props.label && (
+        <Label
+          name={props.name}
+          label={props.label}
+          tooltipText={props.tooltipText}
+        />
+      )}
📝 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.

      {props.label && (
        <Label
          name={props.name}
          label={props.label}
          tooltipText={props.tooltipText}
        />
      )}
apps/web/providers/PostHogProvider.tsx (1)

48-48: 💡 Codebase verification

The opt_in_site_apps configuration appears to be premature

Based on the codebase analysis:

  1. There's no evidence of using any PostHog site apps or experiments currently
  2. The only feature flag usage is minimal (in welcome/form.tsx)
  3. The PostHog integration is primarily used for basic analytics: pageviews, user identification, and event tracking

Consider removing this configuration until there's a concrete need for site apps, as it unnecessarily increases the JavaScript bundle size and potential security surface area.

🔗 Analysis chain

Verify the necessity of enabling PostHog site apps.

While enabling site apps could support the smart categorization feature, please consider:

  1. Security: This increases the exposure surface for analytics data. Ensure this aligns with your privacy policy and data handling requirements.
  2. Performance: Site apps may impact page load times due to additional JavaScript execution.

Could you clarify which specific PostHog site apps/features this will enable for the smart categorization implementation?

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for any PostHog site apps usage or configuration in the codebase
rg -l "posthog.*site.*app" 

Length of output: 28


Script:

#!/bin/bash
# Let's try different search patterns to find PostHog related configurations and usage
rg -l "posthog" --type ts --type tsx

# Also search for any documentation or config files mentioning site apps
rg -l "site.?apps?" --type md --type ts --type tsx

Length of output: 145


Script:

#!/bin/bash
# Search for PostHog related files without type filtering
rg -l "posthog"

# Search for site apps mentions
rg -l "site.?apps?"

# Look specifically at the PostHogProvider file
cat apps/web/providers/PostHogProvider.tsx

Length of output: 2762


Script:

#!/bin/bash
# Let's check the utils/posthog.ts file for any additional PostHog configuration
cat apps/web/utils/posthog.ts

# Check if there are any PostHog feature flags or experiments being used
rg -l "posthog.(use_feature_flag|is_feature_enabled|getFeatureFlag)" 

# Look for any A/B testing or experiment related code
rg "experiment" apps/web/app/

Length of output: 4017

apps/web/app/(app)/smart-categories/setup/SmartCategoriesOnboarding.tsx (2)

18-51: 🛠️ Refactor suggestion

Enhance accessibility and user experience.

While the dialog structure is good, there are opportunities to improve accessibility and user experience.

Consider these improvements:

-    <Dialog open={isOpen} onOpenChange={setIsOpen}>
+    <Dialog 
+      open={isOpen} 
+      onOpenChange={setIsOpen}
+      aria-describedby="smart-categories-description"
+    >
       <DialogContent>
         <DialogHeader>
           <DialogTitle>Welcome to Smart Categories</DialogTitle>
-          <DialogDescription>
+          <DialogDescription id="smart-categories-description">
             Automatically categorize who emails you for better inbox management
             and smarter automation.
           </DialogDescription>
         </DialogHeader>

         <div className="grid gap-2 sm:gap-4">
           <Card className="flex items-center">
-            <TagsIcon className="mr-3 h-5 w-5" />
+            <TagsIcon className="mr-3 h-5 w-5" aria-hidden="true" />
             Auto-categorize who emails you
           </Card>
           <Card className="flex items-center">
-            <ArchiveIcon className="mr-3 h-5 w-5" />
+            <ArchiveIcon className="mr-3 h-5 w-5" aria-hidden="true" />
             Bulk archive by category
           </Card>
           <Card className="flex items-center">
-            <ZapIcon className="mr-3 h-5 w-5" />
+            <ZapIcon className="mr-3 h-5 w-5" aria-hidden="true" />
             Use categories to optimize AI automation
           </Card>
         </div>
         <div>
-          <Button className="w-full" onClick={onClose}>
+          <Button 
+            type="button" 
+            className="w-full" 
+            onClick={onClose}
+          >
             Get Started
           </Button>
         </div>
📝 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.

  return (
    <Dialog 
      open={isOpen} 
      onOpenChange={setIsOpen}
      aria-describedby="smart-categories-description"
    >
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Welcome to Smart Categories</DialogTitle>
          <DialogDescription id="smart-categories-description">
            Automatically categorize who emails you for better inbox management
            and smarter automation.
          </DialogDescription>
        </DialogHeader>

        <div className="grid gap-2 sm:gap-4">
          <Card className="flex items-center">
            <TagsIcon className="mr-3 h-5 w-5" aria-hidden="true" />
            Auto-categorize who emails you
          </Card>
          <Card className="flex items-center">
            <ArchiveIcon className="mr-3 h-5 w-5" aria-hidden="true" />
            Bulk archive by category
          </Card>
          <Card className="flex items-center">
            <ZapIcon className="mr-3 h-5 w-5" aria-hidden="true" />
            Use categories to optimize AI automation
          </Card>
        </div>
        <div>
          <Button 
            type="button" 
            className="w-full" 
            onClick={onClose}
          >
            Get Started
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}

15-17: 🛠️ Refactor suggestion

Consider adding TypeScript types and extracting string literal.

While the implementation is correct, we can improve type safety and maintainability.

Consider applying these improvements:

+const ONBOARDING_KEY = "SmartCategories" as const;
+
 export function SmartCategoriesOnboarding() {
-  const { isOpen, setIsOpen, onClose } = useOnboarding("SmartCategories");
+  const { isOpen, setIsOpen, onClose } = useOnboarding(ONBOARDING_KEY);
📝 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.

const ONBOARDING_KEY = "SmartCategories" as const;

export function SmartCategoriesOnboarding() {
  const { isOpen, setIsOpen, onClose } = useOnboarding(ONBOARDING_KEY);
apps/web/app/(app)/smart-categories/CategorizeWithAiButton.tsx (2)

49-51: 🛠️ Refactor suggestion

Make error message more user-friendly.

The current error message exposes technical details to the user. Consider providing a more user-friendly message while logging the technical details.

-  error: (err) => {
-    return `Error categorizing senders: ${err.message}`;
-  },
+  error: (err) => {
+    console.error("Categorization error:", err);
+    return "Unable to categorize senders. Please try again later.";
+  },
📝 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.

                error: (err) => {
                  console.error("Categorization error:", err);
                  return "Unable to categorize senders. Please try again later.";
                },

20-59: 🛠️ Refactor suggestion

Extract onClick handler to improve readability.

The onClick handler contains complex logic that could be moved to a separate function for better maintainability.

Consider refactoring like this:

+  const handleCategorize = async () => {
+    if (isCategorizing) return;
+    
+    try {
+      setIsCategorizing(true);
+      const result = await handleActionCall(
+        "categorizeSendersAction",
+        categorizeSendersAction,
+      );
+
+      if (isActionError(result)) {
+        throw new Error(result.error);
+      }
+
+      return result;
+    } finally {
+      setIsCategorizing(false);
+    }
+  };

   <Button
     disabled={!hasAiAccess}
-    onClick={async () => {
-      if (isCategorizing) return;
-      toast.promise(...)
-    }}
+    onClick={() => toast.promise(handleCategorize, {
+      loading: "Categorizing senders...",
+      success: "Senders categorized successfully!",
+      error: (err) => `Unable to categorize senders. Please try again later.`,
+    })}
   >
📝 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.

      <PremiumTooltip showTooltip={!hasAiAccess} openModal={openPremiumModal}>
        {/* Extract onClick handler to improve readability */}
        const handleCategorize = async () => {
          if (isCategorizing) return;
          
          try {
            setIsCategorizing(true);
            const result = await handleActionCall(
              "categorizeSendersAction",
              categorizeSendersAction,
            );

            if (isActionError(result)) {
              throw new Error(result.error);
            }

            return result;
          } finally {
            setIsCategorizing(false);
          }
        };

        <Button
          type="button"
          loading={isCategorizing}
          disabled={!hasAiAccess}
          onClick={() => toast.promise(handleCategorize, {
            loading: "Categorizing senders...",
            success: "Senders categorized successfully!",
            error: (err) => `Unable to categorize senders. Please try again later.`,
          })}
        >
          <SparklesIcon className="mr-2 size-4" />
          Categorize Senders with AI
        </Button>
      </PremiumTooltip>
apps/web/components/kanban/TaskCard.tsx (2)

10-14: 🛠️ Refactor suggestion

Consider improving the Task interface

The content field is typed as string but based on usage in EmailCell, it specifically represents an email address.

Consider making this more type-safe:

 export interface Task {
   id: UniqueIdentifier;
   columnId: string;
-  content: string;
+  content: `${string}@${string}`;  // or create an EmailAddress type
 }

Committable suggestion skipped: line range outside the PR's diff.


69-80: 🛠️ Refactor suggestion

Enhance accessibility and user experience

The card's drag handle implementation could be improved.

Consider these improvements:

-      <CardHeader className="space-between relative flex flex-row items-center border-b-2 border-secondary px-3 py-3">
+      <CardHeader 
+        className="space-between relative flex flex-row items-center border-b-2 border-secondary px-3 py-3"
+        role="region"
+        aria-label={`Email from ${task.content}`}
+      >
         <EmailCell emailAddress={task.content} className="mr-auto text-sm" />
         <Button
           variant={"ghost"}
           {...attributes}
           {...listeners}
-          className="-mr-2 h-auto cursor-grab p-1 text-secondary-foreground/50"
+          className="-mr-2 h-auto cursor-grab p-1 text-secondary-foreground/50 hover:bg-secondary/80"
+          aria-label={`Drag email from ${task.content}`}
         >
-          <span className="sr-only">Move email</span>
+          <span className="sr-only">Drag to move email from {task.content}</span>
           <GripVertical />
         </Button>
📝 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.

      <CardHeader 
        className="space-between relative flex flex-row items-center border-b-2 border-secondary px-3 py-3"
        role="region"
        aria-label={`Email from ${task.content}`}
      >
        <EmailCell emailAddress={task.content} className="mr-auto text-sm" />
        <Button
          variant={"ghost"}
          {...attributes}
          {...listeners}
          className="-mr-2 h-auto cursor-grab p-1 text-secondary-foreground/50 hover:bg-secondary/80"
          aria-label={`Drag email from ${task.content}`}
        >
          <span className="sr-only">Drag to move email from {task.content}</span>
          <GripVertical />
        </Button>
      </CardHeader>
apps/web/app/api/user/categorize/senders/find-senders.ts (3)

66-78: 🛠️ Refactor suggestion

Improve type safety of error checking.

The error checking logic could be made more type-safe by defining proper interfaces and using type guards.

Consider this type-safe implementation:

+interface GmailError {
+  errors: Array<{
+    message: string;
+    reason: string;
+  }>;
+}
+
+function isGmailError(error: unknown): error is GmailError {
+  return (
+    typeof error === "object" &&
+    error !== null &&
+    "errors" in error &&
+    Array.isArray((error as any).errors)
+  );
+}
+
 function isNotFoundError(error: unknown): boolean {
-  return (
-    typeof error === "object" &&
-    error !== null &&
-    "errors" in error &&
-    Array.isArray((error as any).errors) &&
-    (error as any).errors.some(
-      (e: any) =>
+  return isGmailError(error) && error.errors.some(
+    (e) =>
         e.message === "Requested entity was not found." &&
-        e.reason === "notFound",
-    )
+        e.reason === "notFound"
   );
 }
📝 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.

interface GmailError {
  errors: Array<{
    message: string;
    reason: string;
  }>;
}

function isGmailError(error: unknown): error is GmailError {
  return (
    typeof error === "object" &&
    error !== null &&
    "errors" in error &&
    Array.isArray((error as any).errors)
  );
}

function isNotFoundError(error: unknown): boolean {
  return isGmailError(error) && error.errors.some(
    (e) =>
      e.message === "Requested entity was not found." &&
      e.reason === "notFound"
  );
}

7-35: ⚠️ Potential issue

Add error handling and input validation.

The pagination implementation looks good, but there are a few concerns:

  1. Missing try-catch block for error handling
  2. No validation for the maxPages parameter
  3. Potential memory issues with large datasets

Consider applying these improvements:

 export async function findSendersWithPagination(
   gmail: gmail_v1.Gmail,
   accessToken: string,
   maxPages: number,
 ) {
+  if (maxPages <= 0) throw new Error("maxPages must be greater than 0");
+  if (maxPages > 100) throw new Error("maxPages cannot exceed 100");
+
   const allSenders: SenderMap = new Map();
   let nextPageToken: string | undefined = undefined;
   let currentPage = 0;

   while (currentPage < maxPages) {
+    try {
       const { senders, nextPageToken: newNextPageToken } = await findSenders(
         gmail,
         accessToken,
         nextPageToken,
       );
+      // Consider implementing batch processing if memory becomes an issue
       for (const [sender, messages] of Object.entries(senders)) {
         const existingMessages = allSenders.get(sender) ?? [];
         allSenders.set(sender, [...existingMessages, ...messages]);
       }

       if (!newNextPageToken) break;
       nextPageToken = newNextPageToken;
       currentPage++;
+    } catch (error) {
+      console.error("Error fetching senders:", error);
+      throw error;
+    }
   }
📝 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.

export async function findSendersWithPagination(
  gmail: gmail_v1.Gmail,
  accessToken: string,
  maxPages: number,
) {
  if (maxPages <= 0) throw new Error("maxPages must be greater than 0");
  if (maxPages > 100) throw new Error("maxPages cannot exceed 100");

  const allSenders: SenderMap = new Map();
  let nextPageToken: string | undefined = undefined;
  let currentPage = 0;

  while (currentPage < maxPages) {
    try {
      const { senders, nextPageToken: newNextPageToken } = await findSenders(
        gmail,
        accessToken,
        nextPageToken,
      );
      // Consider implementing batch processing if memory becomes an issue
      for (const [sender, messages] of Object.entries(senders)) {
        const existingMessages = allSenders.get(sender) ?? [];
        allSenders.set(sender, [...existingMessages, ...messages]);
      }

      if (!newNextPageToken) break;
      nextPageToken = newNextPageToken;
      currentPage++;
    } catch (error) {
      console.error("Error fetching senders:", error);
      throw error;
    }
  }

  return { senders: allSenders, nextPageToken };
}

37-64: 🛠️ Refactor suggestion

Enhance sender extraction and performance.

The function works correctly but could benefit from some improvements:

Consider these enhancements:

 export async function findSenders(
   gmail: gmail_v1.Gmail,
   accessToken: string,
   pageToken?: string,
-  maxResults = 50,
+  maxResults = 50
 ) {
+  if (maxResults > 100) throw new Error("maxResults cannot exceed 100");
   const senders: SenderMap = new Map();

+  try {
     const { threads, nextPageToken } = await getThreadsWithNextPageToken({
       q: "-in:sent",
       gmail,
       maxResults,
       pageToken,
     });

-    const messageIds = threads.map((t) => t.id).filter(isDefined);
+    // Use Set for deduplication of message IDs
+    const messageIds = [...new Set(threads.map((t) => t.id).filter(isDefined))];
     const messages = await getMessagesBatch(messageIds, accessToken);

     for (const message of messages) {
       const sender = message.headers.from;
-      if (sender) {
+      // More robust sender extraction
+      if (sender && sender.includes("@")) {
         const existingMessages = senders.get(sender) ?? [];
         senders.set(sender, [...existingMessages, message]);
       }
     }

     return { senders, nextPageToken };
+  } catch (error) {
+    console.error("Error in findSenders:", error);
+    throw error;
+  }
 }
📝 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.

export async function findSenders(
  gmail: gmail_v1.Gmail,
  accessToken: string,
  pageToken?: string,
  maxResults = 50
) {
  if (maxResults > 100) throw new Error("maxResults cannot exceed 100");
  const senders: SenderMap = new Map();

  try {
    const { threads, nextPageToken } = await getThreadsWithNextPageToken({
      q: "-in:sent",
      gmail,
      maxResults,
      pageToken,
    });

    // Use Set for deduplication of message IDs
    const messageIds = [...new Set(threads.map((t) => t.id).filter(isDefined))];
    const messages = await getMessagesBatch(messageIds, accessToken);

    for (const message of messages) {
      const sender = message.headers.from;
      // More robust sender extraction
      if (sender && sender.includes("@")) {
        const existingMessages = senders.get(sender) ?? [];
        senders.set(sender, [...existingMessages, message]);
      }
    }

    return { senders, nextPageToken };
  } catch (error) {
    console.error("Error in findSenders:", error);
    throw error;
  }
}
apps/web/app/(app)/request-access/page.tsx (2)

16-18: ⚠️ Potential issue

Add interaction handling and feedback mechanisms to the button.

The button appears to be missing crucial functionality:

  1. No click handler to actually enable early access features
  2. No loading state during activation
  3. No success/error feedback to users
  4. Missing accessibility attributes

Consider applying these improvements:

-          <Button id="beta-button" size="xl">
+          <Button
+            id="beta-button"
+            size="xl"
+            onClick={handleEnableEarlyAccess}
+            loading={isLoading}
+            aria-label="Enable early access features"
+          >
             Enable Early Access Features
           </Button>

You'll need to implement the handleEnableEarlyAccess function and manage the loading state. Consider adding a toast notification for success/error feedback.

Committable suggestion skipped: line range outside the PR's diff.


9-18: 💡 Codebase verification

Smart Categories feature needs specific enablement UI

Based on the codebase analysis:

  1. Smart Categories is controlled by its own feature flag (smart-categories) and has dedicated setup pages
  2. The generic "Enable Early Access Features" page doesn't specifically handle Smart Categories enablement
  3. The feature has its own onboarding flow at /smart-categories/setup

Recommendations:

  • Add specific messaging about Smart Categories in the early access page if it's meant to be the entry point
  • Consider adding a dedicated card for Smart Categories similar to the commented-out examples for other features
  • Clarify the relationship between early access enablement and the smart-categories feature flag
🔗 Analysis chain

Verify the feature enablement flow for Smart Categories.

Given that this PR implements Smart Categories functionality, please verify:

  1. Is this the correct entry point for enabling the Smart Categories feature?
  2. Should there be specific messaging about Smart Categories in the description?
  3. Will enabling early access automatically enable Smart Categories?

Let's check for any other Smart Categories related UI components:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for Smart Categories related components and text
echo "Searching for Smart Categories related files..."
rg -l -i "smart.?categor(y|ies)" --type=tsx --type=ts

echo "Searching for other feature enablement components..."
rg -l "enable.*features?" --type=tsx --type=ts

Length of output: 380


Script:

#!/bin/bash
# Search for Smart Categories related components and text with correct file extensions
echo "Searching for Smart Categories related files..."
rg -l -i "smart.?categor(y|ies)"

echo "Searching for early access feature enablement..."
rg -l "early.*access.*feature"

echo "Checking request-access page implementation..."
cat "apps/web/app/(app)/request-access/page.tsx"

Length of output: 3518


Script:

#!/bin/bash
# Check the feature flags implementation and smart categories setup
echo "Checking feature flags implementation..."
cat "apps/web/hooks/useFeatureFlags.ts"

echo "Checking smart categories setup page..."
cat "apps/web/app/(app)/smart-categories/setup/page.tsx"

echo "Checking if there's any connection between early access and smart categories..."
rg -l "useFeatureFlags.*smart" 

Length of output: 1728

apps/web/components/Modal.tsx (1)

69-76: 🛠️ Refactor suggestion

Consider enhancing padding and size management.

While the current implementation works, there are opportunities for improvement:

  1. The padding logic could be more explicit about the default case
  2. The size values could be extracted into constants for better maintainability

Consider applying these improvements:

+ const MODAL_SIZES = {
+   xl: "sm:w-full sm:max-w-xl",
+   "2xl": "sm:w-full sm:max-w-2xl",
+   "4xl": "sm:w-full sm:max-w-4xl",
+   "6xl": "sm:w-full sm:max-w-6xl",
+ } as const;

  className={clsx(
    "w-full transform rounded-2xl bg-white text-left align-middle shadow-xl transition-all",
    {
-     "p-6": padding === "sm",
-     "p-10": !padding,
+     "p-6": padding === "sm",
+     "p-10": padding === undefined,
+     "p-0": padding === "none",
-     "sm:w-full sm:max-w-xl": !fullWidth && (!size || size === "xl"),
-     "sm:w-full sm:max-w-2xl": !fullWidth && size === "2xl",
-     "sm:w-full sm:max-w-4xl": !fullWidth && size === "4xl",
-     "sm:w-full sm:max-w-6xl": !fullWidth && size === "6xl",
+     [MODAL_SIZES[size ?? "xl"]]: !fullWidth,
      "sm:w-full sm:max-w-full": fullWidth,
    }
  )}

Committable suggestion skipped: line range outside the PR's diff.

apps/web/components/OnboardingModal.tsx (1)

91-115: ⚠️ Potential issue

Several improvements needed for the useOnboarding hook.

While the hook implementation is functional, there are several areas for improvement:

  1. The feature parameter should be sanitized before using in localStorage key
  2. The useEffect dependency array is missing setIsOpen
  3. The return object should be memoized to prevent unnecessary rerenders
  4. The feature parameter lacks type safety

Here's the suggested implementation:

-export const useOnboarding = (feature: string) => {
+type OnboardingFeature = 'smartCategories' | 'simpleMode';
+
+export const useOnboarding = (feature: OnboardingFeature) => {
+  // Sanitize feature name for localStorage
+  const storageKey = `viewed${feature.charAt(0).toUpperCase()}${feature.slice(1)}Onboarding`;
+
   const [isOpen, setIsOpen] = useState<boolean>(false);
   const [hasViewedOnboarding, setHasViewedOnboarding] = useLocalStorage(
-    `viewed${feature}Onboarding`,
+    storageKey,
     false,
   );

   useEffect(() => {
     if (!hasViewedOnboarding) {
       setIsOpen(true);
       setHasViewedOnboarding(true);
     }
-  }, [setHasViewedOnboarding, hasViewedOnboarding]);
+  }, [setHasViewedOnboarding, hasViewedOnboarding, setIsOpen]);

   const onClose = useCallback(() => {
     setIsOpen(false);
   }, []);

-  return {
+  return useMemo(() => ({
     isOpen,
     hasViewedOnboarding,
     setIsOpen,
     onClose,
-  };
+  }), [isOpen, hasViewedOnboarding, setIsOpen, onClose]);
 };
📝 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.

type OnboardingFeature = 'smartCategories' | 'simpleMode';

export const useOnboarding = (feature: OnboardingFeature) => {
  // Sanitize feature name for localStorage
  const storageKey = `viewed${feature.charAt(0).toUpperCase()}${feature.slice(1)}Onboarding`;

  const [isOpen, setIsOpen] = useState<boolean>(false);
  const [hasViewedOnboarding, setHasViewedOnboarding] = useLocalStorage(
    storageKey,
    false,
  );

  useEffect(() => {
    if (!hasViewedOnboarding) {
      setIsOpen(true);
      setHasViewedOnboarding(true);
    }
  }, [setHasViewedOnboarding, hasViewedOnboarding, setIsOpen]);

  const onClose = useCallback(() => {
    setIsOpen(false);
  }, []);

  return useMemo(() => ({
    isOpen,
    hasViewedOnboarding,
    setIsOpen,
    onClose,
  }), [isOpen, hasViewedOnboarding, setIsOpen, onClose]);
};
apps/web/__tests__/ai-categorize-senders.test.ts (3)

58-66: 🛠️ Refactor suggestion

Add test cases for additional edge scenarios.

While testing empty senders list is good, consider adding tests for:

  1. Empty categories but non-empty senders
  2. Single sender/category scenarios
  3. Maximum input size limits
it("should handle empty categories list", async () => {
  const senders = ["test@example.com"];
  const result = await aiCategorizeSenders({
    user,
    senders: senders.map(sender => ({ emailAddress: sender, snippet: "" })),
    categories: [],
  });
  expect(result).toEqual([]);
});

68-88: 🛠️ Refactor suggestion

Make category testing more realistic.

The current test creates email addresses directly from category names, which might not reflect real-world scenarios. The AI might rely too heavily on the email address format rather than the actual content.

Consider using more realistic test data:

const testData = [
  {
    sender: "weekly-digest@newsletter.com",
    snippet: "Here's your weekly roundup of top stories",
    expectedCategory: "newsletter"
  },
  // Add more realistic test cases
];

15-56: 🛠️ Refactor suggestion

Enhance test coverage for edge cases and error scenarios.

The current test case focuses on happy path scenarios. Consider adding tests for:

  1. Error handling when AI service fails
  2. Impact of different snippet contents on categorization
  3. Invalid email formats
  4. Rate limiting scenarios

Also, the hardcoded category expectations might make the test brittle if the AI model's behavior changes.

Consider making the test more robust:

 const result = await aiCategorizeSenders({
   user,
-  senders: senders.map((sender) => ({ emailAddress: sender, snippet: "" })),
+  senders: senders.map((sender) => ({ 
+    emailAddress: sender, 
+    snippet: "This is a test email about product updates" 
+  })),
   categories: getEnabledCategories(),
 });

-expect(supportResult?.category).toBe("support");
+expect(supportResult?.category).toMatch(/^(support|customer_service)$/);

Committable suggestion skipped: line range outside the PR's diff.

apps/web/app/api/google/threads/controller.ts (1)

36-47: ⚠️ Potential issue

Add input validation and documentation for the query builder.

While the function effectively consolidates query logic, it needs additional safeguards and documentation:

  1. The query.q and query.fromEmail inputs should be sanitized to prevent Gmail search injection
  2. Consider documenting the function's role in supporting sender categorization

Apply this diff to enhance security and documentation:

+ /**
+  * Constructs a Gmail API query string based on search criteria.
+  * Supports filtering by custom query, sender email, or archive status.
+  * Used for both general search and smart categorization features.
+  */
 function getQuery() {
   if (query.q) {
-    return query.q;
+    return encodeURIComponent(query.q.trim());
   }
   if (query.fromEmail) {
-    return `from:${query.fromEmail}`;
+    // Escape special characters in email addresses
+    return `from:${encodeURIComponent(query.fromEmail.trim())}`;
   }
   if (query.type === "archive") {
     return `-label:${INBOX_LABEL_ID}`;
   }
   return undefined;
 }
📝 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.

  /**
   * Constructs a Gmail API query string based on search criteria.
   * Supports filtering by custom query, sender email, or archive status.
   * Used for both general search and smart categorization features.
   */
  function getQuery() {
    if (query.q) {
      return encodeURIComponent(query.q.trim());
    }
    if (query.fromEmail) {
      // Escape special characters in email addresses
      return `from:${encodeURIComponent(query.fromEmail.trim())}`;
    }
    if (query.type === "archive") {
      return `-label:${INBOX_LABEL_ID}`;
    }
    return undefined;
  }
apps/web/components/CommandK.tsx (1)

71-71: 🛠️ Refactor suggestion

Consider optimizing navigation rendering and adding error handling.

While the implementation is functionally correct, consider these improvements:

  1. Memoize the navigation items mapping to prevent unnecessary re-renders
  2. Add error boundary for navigation state failures

Here's how you could optimize the code:

+ const memoizedNavigationItems = React.useMemo(
+   () =>
+     navigation.map((option) => (
+       <CommandItem
+         key={option.name}
+         onSelect={() => {
+           router.push(option.href);
+           setOpen(false);
+         }}
+       >
+         <option.icon className="mr-2 h-4 w-4" />
+         <span>{option.name}</span>
+       </CommandItem>
+     )),
+   [navigation, router, setOpen]
+ );

  // In the render method, replace the mapping with:
- {navigation.map((option) => (
-   <CommandItem
-     key={option.name}
-     onSelect={() => {
-       router.push(option.href);
-       setOpen(false);
-     }}
-   >
-     <option.icon className="mr-2 h-4 w-4" />
-     <span>{option.name}</span>
-   </CommandItem>
- ))}
+ {memoizedNavigationItems}

And add error handling:

class NavigationErrorBoundary extends React.Component {
  // Add implementation for error boundary
}

// Wrap the navigation group with:
<NavigationErrorBoundary>
  <CommandGroup heading="Navigation">
    {memoizedNavigationItems}
  </CommandGroup>
</NavigationErrorBoundary>

Also applies to: 122-134

apps/web/app/(app)/onboarding/OnboardingBulkUnsubscriber.tsx (1)

154-154: 🛠️ Refactor suggestion

Consider integrating sender categories in the unsubscribe interface.

Since this PR implements AI-based sender categorization, consider enhancing the unsubscribe interface to show the AI-detected category for each sender. This could help users make more informed decisions about which newsletters to unsubscribe from.

Example enhancement:

 <TableRow key={row.name}>
   <TableCell>
     <EmailCell emailAddress={row.name} />
+    {row.category && (
+      <span className="ml-2 text-xs text-gray-500">
+        {row.category}
+      </span>
+    )}
   </TableCell>

Committable suggestion skipped: line range outside the PR's diff.

apps/web/next.config.mjs (1)

160-171: 🛠️ Refactor suggestion

Consider adding more security headers for comprehensive protection.

While the current security headers are good, consider adding these additional headers for enhanced security:

 source: "/(.*)",
 headers: [
   {
     key: "X-Content-Type-Options",
     value: "nosniff",
   },
   {
     key: "X-Frame-Options",
     value: "DENY",
   },
   {
     key: "X-XSS-Protection",
     value: "1; mode=block",
   },
+  {
+    key: "Strict-Transport-Security",
+    value: "max-age=31536000; includeSubDomains",
+  },
+  {
+    key: "Referrer-Policy",
+    value: "strict-origin-when-cross-origin",
+  },
+  {
+    key: "Content-Security-Policy",
+    value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
+  },
 ],

Note: Customize the CSP header value based on your application's specific needs for external resources, inline scripts, and styles.

📝 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.

          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
          {
            key: "X-Frame-Options",
            value: "DENY",
          },
          {
            key: "X-XSS-Protection",
            value: "1; mode=block",
          },
          {
            key: "Strict-Transport-Security",
            value: "max-age=31536000; includeSubDomains",
          },
          {
            key: "Referrer-Policy",
            value: "strict-origin-when-cross-origin",
          },
          {
            key: "Content-Security-Policy",
            value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
          },
apps/web/app/(app)/smart-categories/CreateCategoryButton.tsx (1)

159-165: ⚠️ Potential issue

Reset form validation errors when using example categories.

The handleExampleClick function should reset any existing validation errors when populating the form with example data.

 const handleExampleClick = useCallback(
   (category: ExampleCategory) => {
     setValue("name", category.name);
     setValue("description", category.description);
+    clearErrors(["name", "description"]);
   },
-  [setValue],
+  [setValue, clearErrors],
 );
📝 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.

  const handleExampleClick = useCallback(
    (category: ExampleCategory) => {
      setValue("name", category.name);
      setValue("description", category.description);
      clearErrors(["name", "description"]);
    },
    [setValue, clearErrors],
  );
apps/web/app/(landing)/components/page.tsx (1)

166-181: 🛠️ Refactor suggestion

Enhance the MultiSelectFilter demo with additional states and dynamic options.

While the demo shows basic functionality, consider:

  1. Demonstrating loading and error states
  2. Using the actual category options from a shared constant to prevent inconsistencies
  3. Adding examples of disabled options or maximum selection limits
+ import { EMAIL_CATEGORIES } from "@/constants/categories";

  <div className="mt-4">
+   {/* Basic usage */}
    <MultiSelectFilter
      title="Categories"
-     options={[
-       { label: "Receipts", value: "receipts" },
-       { label: "Newsletters", value: "newsletters" },
-       { label: "Updates", value: "updates" },
-       { label: "Alerts", value: "alerts" },
-     ]}
+     options={EMAIL_CATEGORIES}
      selectedValues={selectedValues}
      setSelectedValues={setSelectedValues}
    />
+   
+   {/* Loading state */}
+   <MultiSelectFilter
+     title="Categories (Loading)"
+     options={EMAIL_CATEGORIES}
+     selectedValues={[]}
+     setSelectedValues={() => {}}
+     isLoading
+   />
+   
+   {/* With disabled options */}
+   <MultiSelectFilter
+     title="Categories (Some Disabled)"
+     options={EMAIL_CATEGORIES.map((opt, i) => ({
+       ...opt,
+       disabled: i === 0,
+     }))}
+     selectedValues={[]}
+     setSelectedValues={() => {}}
+   />
  </div>

Committable suggestion skipped: line range outside the PR's diff.

apps/web/app/(app)/stats/NewsletterModal.tsx (1)

217-220: ⚠️ Potential issue

Fix incorrect empty state message in AllEmails component.

The empty message states "There are no unarchived emails" but this component shows all emails, not just unarchived ones. This could be confusing for users.

Update the message to be more accurate:

  <AlertBasic
    title="No emails"
-   description={`There are no unarchived emails. Switch to the "All" to view all emails from this sender.`}
+   description="No emails found from this sender."
  />

Also applies to: 228-234

apps/web/app/(app)/smart-categories/setup/SetUpCategories.tsx (3)

164-178: ⚠️ Potential issue

Add error handling for category creation.

The category creation process lacks error handling and could leave the component in an inconsistent state if the action fails.

 onClick={async () => {
   setIsCreating(true);
-  const upsertCategories = Array.from(categories.entries()).map(
-    ([name, enabled]) => ({
-      id: combinedCategories.find((c) => c.name === name)?.id,
-      name,
-      enabled,
-    }),
-  );
+  try {
+    const upsertCategories = Array.from(categories.entries()).map(
+      ([name, enabled]) => ({
+        id: combinedCategories.find((c) => c.name === name)?.id,
+        name,
+        enabled,
+      }),
+    );
 
-  await upsertDefaultCategoriesAction(upsertCategories);
-  setIsCreating(false);
-  router.push("/smart-categories");
+    await upsertDefaultCategoriesAction(upsertCategories);
+    router.push("/smart-categories");
+  } catch (error) {
+    console.error("Failed to create categories:", error);
+    // Consider adding a toast notification here
+  } finally {
+    setIsCreating(false);
+  }
 }}
📝 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.

              loading={isCreating}
              onClick={async () => {
                setIsCreating(true);
                try {
                  const upsertCategories = Array.from(categories.entries()).map(
                    ([name, enabled]) => ({
                      id: combinedCategories.find((c) => c.name === name)?.id,
                      name,
                      enabled,
                    }),
                  );

                  await upsertDefaultCategoriesAction(upsertCategories);
                  router.push("/smart-categories");
                } catch (error) {
                  console.error("Failed to create categories:", error);
                  // Consider adding a toast notification here
                } finally {
                  setIsCreating(false);
                }
              }}

202-247: 🛠️ Refactor suggestion

Enhance CategoryCard component with better UX and accessibility.

Consider the following improvements:

  1. Add loading states for actions
  2. Add confirmation dialog for delete action
  3. Improve button labels for accessibility
 function CategoryCard({
   category,
   isEnabled,
   onAdd,
   onRemove,
   onEdit,
 }: {
   category: CardCategory;
   isEnabled: boolean;
   onAdd: () => void;
   onRemove: () => void;
   onEdit: () => void;
 }) {
+  const [isDeleting, setIsDeleting] = useState(false);
+
+  const handleDelete = async () => {
+    if (!confirm(`Are you sure you want to remove ${category.name}?`)) return;
+    setIsDeleting(true);
+    try {
+      await onRemove();
+    } finally {
+      setIsDeleting(false);
+    }
+  };

   return (
     <Card
       className={cn(
         "flex items-center justify-between gap-2 p-4",
         !isEnabled && "bg-gray-50",
       )}
+      aria-label={`Category: ${category.name}`}
     >
       {/* ... rest of the component ... */}
-      <Button size="iconSm" variant="ghost" onClick={onRemove}>
+      <Button 
+        size="iconSm" 
+        variant="ghost" 
+        onClick={handleDelete}
+        disabled={isDeleting}
+        aria-label={`Remove ${category.name} category`}
+      >
         <TrashIcon className="size-4" />
-        <span className="sr-only">Remove</span>
+        <span className="sr-only">Remove {category.name}</span>
       </Button>
     </Card>
   );
 }

Committable suggestion skipped: line range outside the PR's diff.


85-107: ⚠️ Potential issue

Protect against potential race conditions in useEffect.

The category update logic in useEffect could lead to race conditions if existingCategories changes rapidly.

Consider using a cleanup function and adding a mounted flag:

 useEffect(() => {
+  let mounted = true;
   setCategories((prevCategories) => {
+    if (!mounted) return prevCategories;
     const newCategories = new Map(prevCategories);
     // ... existing logic ...
     return newCategories;
   });
+  return () => {
+    mounted = false;
+  };
 }, [existingCategories]);
📝 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.

  useEffect(() => {
    let mounted = true;
    setCategories((prevCategories) => {
      if (!mounted) return prevCategories;
      const newCategories = new Map(prevCategories);

      // Enable any new categories from existingCategories that aren't in the current map
      for (const category of existingCategories) {
        if (!prevCategories.has(category.name)) {
          newCategories.set(category.name, true);
        }
      }

      // Disable any categories that aren't in existingCategories
      if (existingCategories.length) {
        for (const category of prevCategories.keys()) {
          if (!existingCategories.some((c) => c.name === category)) {
            newCategories.set(category, false);
          }
        }
      }

      return newCategories;
    });
    return () => {
      mounted = false;
    };
  }, [existingCategories]);
apps/web/app/(app)/simple/SimpleList.tsx (1)

107-107: ⚠️ Potential issue

Consider potential race condition and callback handling.

The change from an empty callback to undefined is unnecessary since both achieve the same result. More importantly, there's a potential race condition between archiving emails and navigation, as the router navigation occurs regardless of whether the archiving operation succeeds.

Consider these improvements:

-archiveEmails(toArchive, undefined, () => {});
+archiveEmails(toArchive)
+  .then(() => {
+    if (props.nextPageToken) {
+      router.push(`/simple?type=${props.type}&pageToken=${props.nextPageToken}`);
+    } else {
+      const lastCategory = simpleEmailCategoriesArray[simpleEmailCategoriesArray.length - 1][0];
+      if (props.type === lastCategory) {
+        router.push("/simple/completed");
+      } else {
+        const next = getNextCategory(props.type);
+        router.push(`/simple?type=${next}`);
+      }
+    }
+  })
+  .catch((error) => {
+    // Handle archiving errors
+    console.error("Failed to archive emails:", error);
+  });

This change would:

  1. Remove the unnecessary undefined parameter
  2. Ensure navigation only occurs after successful archiving
  3. Add proper error handling

Committable suggestion skipped: line range outside the PR's diff.

apps/web/components/SideNav.tsx (1)

89-95: 🛠️ Refactor suggestion

Consider optimizing the navigation filtering logic.

While the current implementation works, we can make it more maintainable by extracting the path constant.

Consider this optimization:

 export const useNavigation = () => {
   const showSmartCategories = useSmartCategoriesEnabled();
+  const SMART_CATEGORIES_PATH = "/smart-categories";
 
   return navigationItems.filter((item) =>
-    item.href === "/smart-categories" ? showSmartCategories : true,
+    item.href === SMART_CATEGORIES_PATH ? showSmartCategories : true,
   );
 };
📝 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.

export const useNavigation = () => {
  const showSmartCategories = useSmartCategoriesEnabled();
  const SMART_CATEGORIES_PATH = "/smart-categories";

  return navigationItems.filter((item) =>
    item.href === SMART_CATEGORIES_PATH ? showSmartCategories : true,
  );
};
.vscode/typescriptreact.code-snippets (1)

53-53: 💡 Codebase verification

Security check: Inconsistent session validation patterns detected

The codebase shows inconsistent authentication checks across API routes:

  • Most routes use direct NextResponse.json({ error: "Not authenticated" }) without explicit session checks
  • Only one route (cold-email/route.ts) uses the !session?.user pattern
  • Some routes check !user or !userEmail instead
  • One route checks !user?.email

This suggests that the VSCode snippet's security enhancement pattern (!session?.user) is not consistently applied in the actual codebase. The authentication checks should be standardized across all API routes for consistent security enforcement.

🔗 Analysis chain

Security enhancement: Improved session validation in API route snippets.

The authentication checks have been strengthened by validating the presence of a user object within the session. This is a good security practice as it ensures not just that a session exists, but that it contains a valid authenticated user.

Let's verify that this pattern is consistently applied across all API routes in the codebase:

Also applies to: 66-66, 85-85, 225-225

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining basic session checks that might need updating
# Look for potential session checks that don't verify the user object

# Search for session validation patterns in API routes
rg -g '*/api/**/*' 'if.*!session[^?.].*return.*Response'

# Search for NextResponse usage with authentication errors to ensure consistent error handling
rg -g '*/api/**/*' 'NextResponse.*json.*Not authenticated'

Length of output: 417


Script:

#!/bin/bash
# Let's first check if there are any API routes in the codebase
fd "route\.(ts|js|tsx|jsx)$" app

# Then search for session validation patterns in all files
rg "if.*!session.*return.*Response"

# Also search for authentication error responses
rg "NextResponse.*json.*[\"']Not authenticated[\"']"

Length of output: 5719

apps/web/app/(app)/smart-categories/board/page.tsx (4)

28-30: ⚠️ Potential issue

Include uncategorized senders to display all relevant data

The query filters out senders with categoryId: null, but the mapping function handles s.categoryId || "Uncategorized", suggesting that uncategorized senders should be displayed. To ensure all senders are included, update the query to include senders without a category.

Apply this diff to include uncategorized senders:

 prisma.newsletter.findMany({
   where: { userId: session.user.id },
-    categoryId: { not: null } },
   select: { id: true, email: true, categoryId: true },
 }),

Also applies to: 53-53


23-23: 🛠️ Refactor suggestion

Consider redirecting unauthenticated users instead of throwing an error

Throwing an error when the user is not authenticated may lead to an unhandled exception and a less user-friendly experience. Consider redirecting the user to the login page or rendering a friendly message.

Apply this diff to redirect unauthenticated users:

 if (!email) {
-  throw new Error("Not authenticated");
+  redirect('/login');
 }

Ensure you import the redirect function from Next.js:

+import { redirect } from 'next/navigation';

Committable suggestion skipped: line range outside the PR's diff.


35-41: 🛠️ Refactor suggestion

Optimize category ordering logic for better performance

Using categories.find within CATEGORY_ORDER.map can be inefficient for large lists because it performs a search for each category. Consider creating a dictionary for constant-time lookups.

Refactored code:

+ const categoryMap = new Map(categories.map((c) => [c.name, c]));
 const orderedCategories = [
-  ...CATEGORY_ORDER.map((name) =>
-    categories.find((c) => c.name === name),
-  ).filter(isDefined),
+  ...CATEGORY_ORDER.map((name) => categoryMap.get(name)).filter(isDefined),
   ...categories.filter((c) => !CATEGORY_ORDER.includes(c.name)),
 ];
📝 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.

  // Order categories
  const categoryMap = new Map(categories.map((c) => [c.name, c]));
  const orderedCategories = [
    ...CATEGORY_ORDER.map((name) => categoryMap.get(name)).filter(isDefined),
    ...categories.filter((c) => !CATEGORY_ORDER.includes(c.name)),
  ];

25-31: 🛠️ Refactor suggestion

Add error handling for data fetching

The Promise.all may reject if either getUserCategories or the Prisma query fails. Consider wrapping the data fetching in a try-catch block to handle potential errors gracefully.

Example:

+try {
   const [categories, senders] = await Promise.all([
     getUserCategories(session.user.id),
     prisma.newsletter.findMany({
       where: { userId: session.user.id },
       select: { id: true, email: true, categoryId: true },
     }),
   ]);
+} catch (error) {
+  // Handle errors appropriately
+  console.error(error);
+  return <div className="p-4">An error occurred while fetching data.</div>;
+}

Committable suggestion skipped: line range outside the PR's diff.

apps/web/app/api/user/categorize/senders/uncategorized/route.ts (5)

64-68: 🛠️ Refactor suggestion

Return appropriate HTTP status codes for error responses

Providing specific HTTP status codes with error responses helps clients handle errors correctly and improves the API's clarity. Consider returning status codes like 401 for authentication errors and 500 for server errors.

Apply the following changes:

 if (!user?.email)
-  return NextResponse.json({ error: "Not authenticated" });
+  return NextResponse.json({ error: "Not authenticated" }, { status: 401 });

 if (error)
-  return NextResponse.json({ error });
+  return NextResponse.json({ error }, { status: 500 });

 if (!gmail)
-  return NextResponse.json({ error: "Could not load Gmail" });
+  return NextResponse.json({ error: "Could not load Gmail" }, { status: 500 });

 if (!session?.accessToken)
-  return NextResponse.json({ error: "No access token" });
+  return NextResponse.json({ error: "No access token" }, { status: 401 });
📝 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.

  if (!user?.email)
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  if (error)
    return NextResponse.json({ error }, { status: 500 });
  if (!gmail)
    return NextResponse.json({ error: "Could not load Gmail" }, { status: 500 });
  if (!session?.accessToken)
    return NextResponse.json({ error: "No access token" }, { status: 401 });

27-31: ⚠️ Potential issue

Add error handling for getSenders call

The asynchronous call to getSenders may throw an error, which could cause the function to fail unexpectedly. To handle potential errors gracefully, wrap the call in a try-catch block.

Apply the following changes:

 while (uncategorizedSenders.length === 0) {
+  try {
     const result = await getSenders({
       ownerEmail: email,
       limit,
       offset: currentOffset,
     });
+  } catch (error) {
+    console.error("Error fetching senders:", error);
+    // Optionally, handle the error or rethrow it
+    throw error;
+  }
   const allSenders = result.data.map((sender) => sender.from);
   // ... rest of the loop ...
 }

Committable suggestion skipped: line range outside the PR's diff.


71-72: ⚠️ Potential issue

Validate the offset parameter to ensure it's a valid number

Currently, the offset parameter is parsed without validation. If an invalid value is provided, offset could be NaN, potentially leading to unexpected behavior. Add validation to ensure offset is a non-negative integer.

Apply the following changes:

 const url = new URL(request.url);
 const offsetParam = url.searchParams.get("offset") || "0";
-const offset = Number.parseInt(offsetParam);
+const offset = Number.parseInt(offsetParam, 10);
+if (isNaN(offset) || offset < 0) {
+  return NextResponse.json({ error: "Invalid offset parameter" }, { status: 400 });
+}

Committable suggestion skipped: line range outside the PR's diff.


63-68: ⚠️ Potential issue

Reorder error checks for proper error handling

To prevent accessing properties of undefined objects, check for error before accessing user or gmail. This ensures that if an error occurred during session retrieval, it is handled appropriately before other checks.

Apply the following changes:

 export const GET = withError(async (request: Request) => {
   const { gmail, user, error, session } = await getSessionAndGmailClient();
+  if (error) return NextResponse.json({ error }, { status: 500 });
   if (!user?.email) return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
-  if (error) return NextResponse.json({ error });
   if (!gmail) return NextResponse.json({ error: "Could not load Gmail" }, { status: 500 });
   if (!session?.accessToken)
     return NextResponse.json({ error: "No access token" }, { status: 401 });
📝 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.

  const { gmail, user, error, session } = await getSessionAndGmailClient();
  if (error) return NextResponse.json({ error }, { status: 500 });
  if (!user?.email) return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  if (!gmail) return NextResponse.json({ error: "Could not load Gmail" }, { status: 500 });
  if (!session?.accessToken)
    return NextResponse.json({ error: "No access token" }, { status: 401 });

26-54: ⚠️ Potential issue

Ensure the while loop terminates appropriately

The current while loop could potentially iterate indefinitely if uncategorizedSenders remains empty and getSenders continues to return senders already categorized. To prevent excessive looping or a potential infinite loop, consider adding a maximum number of iterations or a condition to break the loop after a certain point.

Apply the following changes:

 let uncategorizedSenders: string[] = [];
 let currentOffset = offset;
+ const maxIterations = 10;
+ let iterations = 0;

 while (uncategorizedSenders.length === 0) {
+  if (iterations >= maxIterations) {
+    // Prevent infinite loop if no uncategorized senders are found
+    return { uncategorizedSenders };
+  }
   const result = await getSenders({
     ownerEmail: email,
     limit,
     offset: currentOffset,
   });
   // ... rest of the loop ...
   currentOffset += limit;
+  iterations += 1;
 }

Committable suggestion skipped: line range outside the PR's diff.

apps/web/components/kanban/multipleContainersKeyboardPreset.ts (1)

29-83: ⚠️ Potential issue

Replace return statements inside the loop with continue to prevent unintended function exits

The return statements within the loop (lines 30-31, 36-37, 47-48, 55-56, 64-65) cause the entire coordinateGetter function to exit prematurely when certain conditions are met. If the intention is to skip the current iteration and proceed with the next entry, continue should be used instead of return.

Apply this diff to fix the issue:

@@ -30,7 +30,7 @@
       if (!entry || entry?.disabled) {
-        return;
+        continue;
       }

@@ -36,7 +36,7 @@
       if (!rect) {
-        return;
+        continue;
       }

@@ -47,7 +47,7 @@
         if (active.data.current?.type !== "Column") {
-          return;
+          continue;
         }
       }

@@ -55,7 +55,7 @@
         if (active.data.current?.type === "Column") {
-          return;
+          continue;
         }

@@ -64,7 +64,7 @@
         if (active.data.current?.type === "Column") {
-          return;
+          continue;
         }
📝 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.

    for (const entry of droppableContainers.getEnabled()) {
      if (!entry || entry?.disabled) {
        continue;
      }

      const rect = droppableRects.get(entry.id);

      if (!rect) {
        continue;
      }

      const data = entry.data.current;

      if (data) {
        const { type, children } = data;

        if (type === "Column" && children?.length > 0) {
          if (active.data.current?.type !== "Column") {
            continue;
          }
        }
      }

      switch (event.code) {
        case KeyboardCode.Down:
          if (active.data.current?.type === "Column") {
            continue;
          }
          if (collisionRect.top < rect.top) {
            // find all droppable areas below
            filteredContainers.push(entry);
          }
          break;
        case KeyboardCode.Up:
          if (active.data.current?.type === "Column") {
            continue;
          }
          if (collisionRect.top > rect.top) {
            // find all droppable areas above
            filteredContainers.push(entry);
          }
          break;
        case KeyboardCode.Left:
          if (collisionRect.left >= rect.left + rect.width) {
            // find all droppable areas to left
            filteredContainers.push(entry);
          }
          break;
        case KeyboardCode.Right:
          // find all droppable areas to right
          if (collisionRect.left + collisionRect.width <= rect.left) {
            filteredContainers.push(entry);
          }
          break;
      }
apps/web/store/ai-categorize-sender-queue.ts (4)

11-15: 🛠️ Refactor suggestion

Extend 'CategorizationStatus' and 'QueueItem' to handle failed tasks

To properly manage tasks that fail after all retry attempts, consider adding a 'failed' status to CategorizationStatus and an optional error property to QueueItem.

Update the type and interface:

type CategorizationStatus = "pending" | "processing" | "completed";
+ | "failed";

interface QueueItem {
  status: CategorizationStatus;
  categoryId?: string;
+ error?: string;
}
📝 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.

type CategorizationStatus = "pending" | "processing" | "completed" | "failed";

interface QueueItem {
  status: CategorizationStatus;
  categoryId?: string;
  error?: string;
}

81-88: 🛠️ Refactor suggestion

Remove completed and failed items from the queue to prevent memory buildup

Currently, completed and failed items remain in aiCategorizeSenderQueueAtom, which may lead to memory issues over time. Consider removing items with terminal statuses from the queue.

Adjust the code to remove items when they reach a terminal status:

jotaiStore.set(aiCategorizeSenderQueueAtom, (prev) => {
  const newQueue = new Map(prev);
- newQueue.set(sender, {
-   status: "completed",
-   categoryId: result.categoryId || undefined,
- });
+ newQueue.delete(sender);
  return newQueue;
});

Alternatively, implement a cleanup mechanism that periodically clears items with statuses 'completed' or 'failed'.

📝 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.

        jotaiStore.set(aiCategorizeSenderQueueAtom, (prev) => {
          const newQueue = new Map(prev);
          newQueue.delete(sender);
          return newQueue;
        });

60-95: 🛠️ Refactor suggestion

Consider processing tasks in batches to improve performance

If the number of senders is large, creating a task for each sender individually could impact performance. Processing senders in batches may improve efficiency.

Modify the processAiCategorizeSenderQueue function to handle batching:

function processAiCategorizeSenderQueue({ senders }: { senders: string[] }) {
- const tasks = senders.map((sender) => async () => {
+ const batchSize = 10; // Adjust based on optimal batch size
+ for (let i = 0; i < senders.length; i += batchSize) {
+   const batch = senders.slice(i, i + batchSize);
+   const tasks = batch.map((sender) => async () => {
      // existing task code
    });
+   aiQueue.addAll(tasks);
+ }
}

This approach reduces the overhead of managing a large number of individual tasks.

Committable suggestion skipped: line range outside the PR's diff.


68-91: ⚠️ Potential issue

Handle failures after maximum retry attempts

If categorizeSenderAction continues to fail after all retries, the error is unhandled. This could lead to unnoticed failures in processing. Consider updating the item's status to 'failed' and storing the error message.

Modify the code to handle failures:

await pRetry(
  async (attemptCount) => {
    // existing code
  },
  { retries: 3 },
).catch((error) => {
+ jotaiStore.set(aiCategorizeSenderQueueAtom, (prev) => {
+   const newQueue = new Map(prev);
+   newQueue.set(sender, {
+     status: "failed",
+     error: error.message,
+   });
+   return newQueue;
+ });
});
📝 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.

    await pRetry(
      async (attemptCount) => {
        console.log(
          `Queue: aiCategorizeSender. Processing ${sender}${attemptCount > 1 ? ` (attempt ${attemptCount})` : ""}`,
        );

        const result = await categorizeSenderAction(sender);

        if (isActionError(result)) {
          await sleep(exponentialBackoff(attemptCount, 1_000));
          throw new Error(result.error);
        }

        jotaiStore.set(aiCategorizeSenderQueueAtom, (prev) => {
          const newQueue = new Map(prev);
          newQueue.set(sender, {
            status: "completed",
            categoryId: result.categoryId || undefined,
          });
          return newQueue;
        });
      },
      { retries: 3 },
    ).catch((error) => {
      jotaiStore.set(aiCategorizeSenderQueueAtom, (prev) => {
        const newQueue = new Map(prev);
        newQueue.set(sender, {
          status: "failed",
          error: error.message,
        });
        return newQueue;
      });
    });
apps/web/store/archive-sender-queue.ts (3)

98-101: 🛠️ Refactor suggestion

Simplify useArchiveSenderStatus by removing unnecessary useMemo

The useMemo hook may not be necessary here since getStatus(sender) is a simple retrieval and not computationally intensive. Removing useMemo simplifies the code without impacting performance.

Apply this diff:

 export const useArchiveSenderStatus = (sender: string) => {
   const getStatus = useAtomValue(archiveSenderStatusAtom);
-  return useMemo(() => getStatus(sender), [getStatus, sender]);
+  return getStatus(sender);
 };
📝 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.

export const useArchiveSenderStatus = (sender: string) => {
  const getStatus = useAtomValue(archiveSenderStatusAtom);
  return getStatus(sender);
};

35-36: ⚠️ Potential issue

Possible mismatch between expected data structure from fetchSenderThreads

The variable threads is expected to be an array, but fetchSenderThreads might be returning an object. Ensure that fetchSenderThreads returns an array of threads. If GetThreadsResponse contains a threads property, you should adjust the code accordingly.

Apply this diff to adjust the return value of fetchSenderThreads:

 async function fetchSenderThreads(sender: string) {
   const url = `/api/google/threads/basic?from=${encodeURIComponent(sender)}&labelId=INBOX`;
   const res = await fetch(url);

   if (!res.ok) throw new Error(`Failed to fetch threads: ${res.status} ${res.statusText}`);

   const data: GetThreadsResponse = await res.json();

-  return data;
+  return data.threads;
 }

Committable suggestion skipped: line range outside the PR's diff.


93-96: 🛠️ Refactor suggestion

Consider using atomFamily for parameterized atoms

Instead of returning a function from archiveSenderStatusAtom, you could use atomFamily from Jotai to create parameterized atoms. This approach can improve readability and align with best practices for state management.

Here's how you could refactor the code:

-import { atom, useAtomValue } from "jotai";
+import { atom, useAtomValue, atomFamily } from "jotai";
+import { atomFamily } from "jotai/utils";

-const archiveSenderStatusAtom = atom((get) => {
-  const queue = get(archiveSenderQueueAtom);
-  return (sender: string) => queue.get(sender);
-});

+const archiveSenderStatusFamily = atomFamily((sender: string) => atom((get) => {
+  const queue = get(archiveSenderQueueAtom);
+  return queue.get(sender);
+}));

 export const useArchiveSenderStatus = (sender: string) => {
-  const getStatus = useAtomValue(archiveSenderStatusAtom);
-  return useMemo(() => getStatus(sender), [getStatus, sender]);
+  return useAtomValue(archiveSenderStatusFamily(sender));
 };

Note: Ensure you have jotai/utils installed to use atomFamily.

Committable suggestion skipped: line range outside the PR's diff.

apps/web/components/kanban/BoardColumn.tsx (2)

58-60: 🛠️ Refactor suggestion

Consider making column dimensions responsive for better adaptability.

The hardcoded height of 800px and width of 350px may not display well on all screen sizes. Consider using responsive units or CSS techniques like max-width, flex, or media queries to enhance responsiveness across different devices.


48-50: ⚠️ Potential issue

Fix incorrect accessibility attribute 'roleDescription'.

The attribute roleDescription should be aria-roledescription to comply with ARIA standards and ensure proper accessibility support.

Apply this diff to correct the attribute:

 attributes: {
-  roleDescription: `Column: ${column.title}`,
+  'aria-roledescription': `Column: ${column.title}`,
 },
📝 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.

    attributes: {
      'aria-roledescription': `Column: ${column.title}`,
    },
apps/web/app/(app)/smart-categories/page.tsx (3)

37-51: ⚠️ Potential issue

Add error handling for data fetching operations

When using Promise.all, if any of the promises reject, it could lead to unhandled exceptions. To enhance robustness, consider wrapping the data fetching in a try-catch block or handling errors individually.

Would you like assistance in implementing error handling for these asynchronous operations?


103-106: ⚠️ Potential issue

Handle possible undefined values when sorting senders

When sorting senders by sender.category?.name, if a sender does not have a category, sender.category?.name will be undefined, which may affect the sorting order. To ensure consistent sorting, provide a default value.

Apply this change:

sortBy(
  senders,
- (sender) => sender.category?.name,
+ (sender) => sender.category?.name || '',
)
📝 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.

                emailGroups={sortBy(
                  senders,
                  (sender) => sender.category?.name || '',
                ).map((sender) => ({

35-35: 🛠️ Refactor suggestion

Refactor authentication error handling to redirect unauthenticated users

Instead of throwing an error when the user is not authenticated, consider redirecting them to the login page for a better user experience.

Apply this change:

- if (!email) throw new Error("Not authenticated");
+ if (!email) redirect("/login");

Committable suggestion skipped: line range outside the PR's diff.

apps/web/store/archive-queue.ts (3)

181-182: ⚠️ Potential issue

Reconsider removing failed threads from the queue

Currently, removeThreadFromQueue(threadId, actionType); is called regardless of whether the action succeeded or failed. This means that threads that failed to process after all retries are removed from the queue and won't be retried later. If it's important to ensure that all threads are processed successfully, consider keeping failed threads in the queue or implementing a mechanism to retry them later.


44-49: ⚠️ Potential issue

Incorrect usage of atomWithStorage: invalid options parameter

The atomWithStorage function does not accept an options parameter as a fourth argument. The correct usage is atomWithStorage(key, initialValue, storage?). Passing { getOnInit: true } as a fourth argument may lead to unexpected behavior.

Apply this diff to correct the function call:

-const queueAtom = atomWithStorage(
-  "gmailActionQueue",
-  { activeThreads: {}, totalThreads: 0 },
-  createStorage(),
-  { getOnInit: true },
-);
+const queueAtom = atomWithStorage(
+  "gmailActionQueue",
+  { activeThreads: {}, totalThreads: 0 },
+  createStorage(),
+);
📝 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.

const queueAtom = atomWithStorage(
  "gmailActionQueue",
  { activeThreads: {}, totalThreads: 0 },
  createStorage(),
);

129-143: 🛠️ Refactor suggestion

Update totalThreads when removing threads from the queue

In removeThreadFromQueue, the totalThreads value is not updated when a thread is removed from activeThreads. If totalThreads is intended to represent the total number of threads to be processed for progress tracking, consider decrementing it when a thread is removed from the queue.

Apply this diff to adjust totalThreads:

function removeThreadFromQueue(threadId: string, actionType: ActionType) {
  jotaiStore.set(queueAtom, (prev) => {
    const remainingThreads = Object.fromEntries(
      Object.entries(prev.activeThreads).filter(
        ([_key, value]) =>
          !(value.threadId === threadId && value.actionType === actionType),
      ),
    );

    return {
      ...prev,
      activeThreads: remainingThreads,
+     totalThreads: prev.totalThreads - 1,
    };
  });
}
📝 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.

function removeThreadFromQueue(threadId: string, actionType: ActionType) {
  jotaiStore.set(queueAtom, (prev) => {
    const remainingThreads = Object.fromEntries(
      Object.entries(prev.activeThreads).filter(
        ([_key, value]) =>
          !(value.threadId === threadId && value.actionType === actionType),
      ),
    );

    return {
      ...prev,
      activeThreads: remainingThreads,
      totalThreads: prev.totalThreads - 1,
    };
  });
}
apps/web/components/MultiSelectFilter.tsx (2)

24-34: ⚠️ Potential issue

Remove unused generic type parameters TData and TValue

The generic type parameters TData and TValue in the MultiSelectFilterProps interface and MultiSelectFilter function are not utilized within the code. Removing them can simplify the code and prevent potential confusion.

Apply this diff to remove the unused generic type parameters:

-interface MultiSelectFilterProps<TData, TValue> {
+interface MultiSelectFilterProps {
   title?: string;
   options: {
     label: string;
     value: string;
     icon?: React.ComponentType<{ className?: string }>;
   }[];
   selectedValues: Set<string>;
   setSelectedValues: (values: Set<string>) => void;
   maxDisplayedValues?: number;
 }

-export function MultiSelectFilter<TData, TValue>({
+export function MultiSelectFilter({
   title,
   options,
   selectedValues,
   setSelectedValues,
   maxDisplayedValues,
 }: MultiSelectFilterProps) {

Also applies to: 36-42


24-34: ⚠️ Potential issue

Ensure consistency in options type between component and hook

In MultiSelectFilterProps, the options prop is an array of objects with label, value, and optional icon. However, in useMultiSelectFilter, the options parameter is an array of strings. This inconsistency may lead to confusion or errors when integrating the hook with the component. Consider aligning the options type between the component and the hook.

Apply the following changes to useMultiSelectFilter to accept the same options type and initialize selectedValues accordingly:

-export function useMultiSelectFilter(options: string[]) {
+export function useMultiSelectFilter(options: {
+  label: string;
+  value: string;
+  icon?: React.ComponentType<{ className?: string }>;
+}[]) {
   const [selectedValues, setSelectedValues] = React.useState<Set<string>>(
-    new Set(options),
+    new Set(options.map((option) => option.value)),
   );
   return { selectedValues, setSelectedValues };
 }

Also applies to: 156-161

apps/web/components/kanban/KanbanBoard.tsx (3)

253-268: 🛠️ Refactor suggestion

Use a safer check for window existence before using createPortal

The current check {"document" in window} may not reliably determine if the code is running in a browser environment. A more robust check is to verify if typeof window !== 'undefined'. This helps prevent errors during server-side rendering.

Apply this diff to update the check:

- {"document" in window &&
+ {typeof window !== 'undefined' &&
    createPortal(
      <DragOverlay>
        {activeColumn && (
          <BoardColumn
            isOverlay
            column={activeColumn}
            tasks={tasks.filter(
              (task) => task.columnId === activeColumn.id,
            )}
          />
        )}
        {activeTask && <TaskCard task={activeTask} isOverlay />}
      </DragOverlay>,
      document.body,
    )}
📝 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.

      {typeof window !== 'undefined' &&
        createPortal(
          <DragOverlay>
            {activeColumn && (
              <BoardColumn
                isOverlay
                column={activeColumn}
                tasks={tasks.filter(
                  (task) => task.columnId === activeColumn.id,
                )}
              />
            )}
            {activeTask && <TaskCard task={activeTask} isOverlay />}
          </DragOverlay>,
          document.body,
        )}

286-312: ⚠️ Potential issue

Handle task drops in the onDragEnd function

The onDragEnd function currently only handles dragging and dropping of columns. If a task is dragged and dropped, the function returns early and does not update the tasks state accordingly. This means that tasks may not move correctly between columns or positions upon drop.

Apply this diff to handle task drops:

 function onDragEnd(event: DragEndEvent) {
   setActiveColumn(null);
   setActiveTask(null);

   const { active, over } = event;
   if (!over) return;

   const activeId = active.id;
   const overId = over.id;

   if (!hasDraggableData(active)) return;

   const activeData = active.data.current;

   if (activeId === overId) return;

   const isActiveAColumn = activeData?.type === "Column";
+  const isActiveATask = activeData?.type === "Task";

-  if (!isActiveAColumn) return;
+  if (isActiveAColumn) {
     setColumns((columns) => {
       const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
       const overColumnIndex = columns.findIndex((col) => col.id === overId);
       return arrayMove(columns, activeColumnIndex, overColumnIndex);
     });
+  } else if (isActiveATask) {
+    setTasks((tasks) => {
+      const activeIndex = tasks.findIndex((t) => t.id === activeId);
+      const overIndex = tasks.findIndex((t) => t.id === overId);
+      const activeTask = tasks[activeIndex];
+      const overTask = tasks[overIndex];
+      if (activeTask && overTask) {
+        const updatedTask = {
+          ...activeTask,
+          columnId: overTask.columnId,
+        };
+        const updatedTasks = tasks.slice();
+        updatedTasks[activeIndex] = updatedTask;
+        return arrayMove(updatedTasks, activeIndex, overIndex);
+      }
+      return tasks;
+    });
   }
 }
📝 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.

  function onDragEnd(event: DragEndEvent) {
    setActiveColumn(null);
    setActiveTask(null);

    const { active, over } = event;
    if (!over) return;

    const activeId = active.id;
    const overId = over.id;

    if (!hasDraggableData(active)) return;

    const activeData = active.data.current;

    if (activeId === overId) return;

    const isActiveAColumn = activeData?.type === "Column";
    const isActiveATask = activeData?.type === "Task";

    if (isActiveAColumn) {
      setColumns((columns) => {
        const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
        const overColumnIndex = columns.findIndex((col) => col.id === overId);
        return arrayMove(columns, activeColumnIndex, overColumnIndex);
      });
    } else if (isActiveATask) {
      setTasks((tasks) => {
        const activeIndex = tasks.findIndex((t) => t.id === activeId);
        const overIndex = tasks.findIndex((t) => t.id === overId);
        const activeTask = tasks[activeIndex];
        const overTask = tasks[overIndex];
        if (activeTask && overTask) {
          const updatedTask = {
            ...activeTask,
            columnId: overTask.columnId,
          };
          const updatedTasks = tasks.slice();
          updatedTasks[activeIndex] = updatedTask;
          return arrayMove(updatedTasks, activeIndex, overIndex);
        }
        return tasks;
      });
    }
  }

335-366: ⚠️ Potential issue

Avoid mutating state directly when updating tasks in onDragOver

In the onDragOver function, you're directly mutating task objects within the state array (e.g., activeTask.columnId = overTask.columnId;). Directly mutating state can lead to unexpected behavior in React. Instead, create new task objects when updating them to adhere to React's immutability principles.

Apply these changes to prevent direct state mutation:

// When dropping a Task over another Task
if (isActiveATask && isOverATask) {
  setTasks((tasks) => {
    const activeIndex = tasks.findIndex((t) => t.id === activeId);
    const overIndex = tasks.findIndex((t) => t.id === overId);
    const activeTask = tasks[activeIndex];
    const overTask = tasks[overIndex];
    if (
      activeTask &&
      overTask &&
      activeTask.columnId !== overTask.columnId
    ) {
-     activeTask.columnId = overTask.columnId;
+     const updatedTask = {
+       ...activeTask,
+       columnId: overTask.columnId,
+     };
+     const updatedTasks = tasks.slice();
+     updatedTasks[activeIndex] = updatedTask;
+     return arrayMove(updatedTasks, activeIndex, overIndex - 1);
    }
    return arrayMove(tasks, activeIndex, overIndex);
  });
}

// When dropping a Task over a Column
if (isActiveATask && isOverAColumn) {
  setTasks((tasks) => {
    const activeIndex = tasks.findIndex((t) => t.id === activeId);
    const activeTask = tasks[activeIndex];
    if (activeTask) {
+     const updatedTask = {
+       ...activeTask,
+       columnId: overId as string,
+     };
+     const updatedTasks = tasks.slice();
+     updatedTasks[activeIndex] = updatedTask;
+     return updatedTasks;
-     activeTask.columnId = overId as string;
-     return arrayMove(tasks, activeIndex, activeIndex);
    }
    return tasks;
  });
}
📝 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.

      setTasks((tasks) => {
        const activeIndex = tasks.findIndex((t) => t.id === activeId);
        const overIndex = tasks.findIndex((t) => t.id === overId);
        const activeTask = tasks[activeIndex];
        const overTask = tasks[overIndex];
        if (
          activeTask &&
          overTask &&
          activeTask.columnId !== overTask.columnId
        ) {
          const updatedTask = {
            ...activeTask,
            columnId: overTask.columnId,
          };
          const updatedTasks = tasks.slice();
          updatedTasks[activeIndex] = updatedTask;
          return arrayMove(updatedTasks, activeIndex, overIndex - 1);
        }

        return arrayMove(tasks, activeIndex, overIndex);
      });
    }

    const isOverAColumn = overData?.type === "Column";

    // Im dropping a Task over a column
    if (isActiveATask && isOverAColumn) {
      setTasks((tasks) => {
        const activeIndex = tasks.findIndex((t) => t.id === activeId);
        const activeTask = tasks[activeIndex];
        if (activeTask) {
          const updatedTask = {
            ...activeTask,
            columnId: overId as string,
          };
          const updatedTasks = tasks.slice();
          updatedTasks[activeIndex] = updatedTask;
          return updatedTasks;
        }
        return tasks;
      });
    }
apps/web/app/api/google/webhook/process-history.ts (4)

339-346: ⚠️ Potential issue

Ensure message.headers.from and message.headers.date are defined before use

To prevent potential runtime errors, add checks to ensure that message.headers.from and message.headers.date are defined before using them in hasPreviousEmailsFromSenderOrDomain.


371-371: ⚠️ Potential issue

Add null check for message.headers.from

Before assigning message.headers.from to sender, ensure it is not undefined to avoid possible null reference errors.


372-378: 🛠️ Refactor suggestion

Address potential race conditions when categorizing senders

Concurrent processing of messages from the same sender could lead to race conditions, resulting in duplicate categorization attempts. Implement a unique constraint on the newsletter table for email_userId and handle duplicate key errors gracefully to prevent this issue.

Consider adding a unique constraint in your Prisma schema:

model Newsletter {
  // existing fields
  email     String
  userId    String
  // other fields

  @@unique([email, userId])
}

Handle the potential conflict in code:

if (!existingSender?.category) {
+  try {
     await categorizeSender(sender, user, gmail, accessToken);
+  } catch (error) {
+    if (error.code === 'P2002') {
+      // Unique constraint failed, another process inserted the sender
+      console.warn(`Sender already categorized by another process: ${sender}`);
+    } else {
+      throw error;
+    }
+  }
}

376-378: ⚠️ Potential issue

Handle exceptions during sender categorization

Wrap the categorizeSender call in a try-catch block to handle any exceptions that may occur during the categorization process, ensuring the webhook processing continues smoothly.

Apply this diff to handle exceptions:

if (!existingSender?.category) {
+  try {
     await categorizeSender(sender, user, gmail, accessToken);
+  } catch (error) {
+    console.error(`Error categorizing sender: ${sender}`, error);
+    // Optionally, capture the exception or handle it accordingly
+  }
}
📝 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.

      if (!existingSender?.category) {
        try {
          await categorizeSender(sender, user, gmail, accessToken);
        } catch (error) {
          console.error(`Error categorizing sender: ${sender}`, error);
          // Optionally, capture the exception or handle it accordingly
        }
      }
apps/web/components/GroupedTable.tsx (5)

172-176: 🛠️ Refactor suggestion

Optimize archiving senders by processing concurrently

The onArchiveAll function currently archives senders sequentially, which can be slow if there are many senders to process. To improve performance, consider processing all senders concurrently using Promise.all.

Apply this diff to process senders concurrently:

const onArchiveAll = async () => {
-  for (const sender of senders) {
-    await addToArchiveSenderQueue(sender.address);
-  }
+  await Promise.all(
+    senders.map((sender) => addToArchiveSenderQueue(sender.address))
+  );
};
📝 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.

          const onArchiveAll = async () => {
            await Promise.all(
              senders.map((sender) => addToArchiveSenderQueue(sender.address))
            );
          };

433-437: ⚠️ Potential issue

Ensure 'categoryId' is correctly typed in 'changeSenderCategoryAction'

In the onValueChange handler of SelectCategoryCell, value is a string, but if changeSenderCategoryAction expects categoryId as a number, you should convert value to an integer to prevent type mismatches.

Apply this diff to fix the type:

onValueChange={async (value) => {
  const result = await changeSenderCategoryAction({
    sender,
-   categoryId: value,
+   categoryId: parseInt(value, 10),
  });
  if (isActionError(result)) {
    toastError({ description: result.error });
  } else {
    toastSuccess({ description: "Category changed" });
  }
}}
📝 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.

      onValueChange={async (value) => {
        const result = await changeSenderCategoryAction({
          sender,
          categoryId: parseInt(value, 10),
        });
        if (isActionError(result)) {
          toastError({ description: result.error });
        } else {
          toastSuccess({ description: "Category changed" });
        }
      }}

463-479: ⚠️ Potential issue

Add null checks for 'status' properties in 'ArchiveStatusCell'

In the ArchiveStatusCell component, you're using status.threadsTotal and status.threadIds.length without checking if they are defined. To prevent potential runtime errors, add null checks before performing arithmetic operations or accessing properties.

Apply this diff to add null checks:

switch (status?.status) {
  // ...
  case "processing":
    return (
      <span className="text-blue-500">
-       Archiving... {status.threadsTotal - status.threadIds.length} /{" "}
-       {status.threadsTotal}
+       Archiving... {status.threadsTotal && status.threadIds
+         ? status.threadsTotal - status.threadIds.length
+         : 0}{" "}
+       / {status.threadsTotal || 0}
      </span>
    );
  // ...
}
📝 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.

  switch (status?.status) {
    case "completed":
      if (status.threadsTotal) {
        return (
          <span className="text-green-500">
            Archived {status.threadsTotal} emails!
          </span>
        );
      }
      return <span className="text-gray-500">Archived</span>;
    case "processing":
      return (
        <span className="text-blue-500">
          Archiving... {status.threadsTotal && status.threadIds
            ? status.threadsTotal - status.threadIds.length
            : 0}{" "}
          / {status.threadsTotal || 0}
        </span>
      );

124-154: ⚠️ Potential issue

Change accessorKey from "date" to "category" for clarity

In the columns definition, the accessorKey for the category selection column is set to "date", which is misleading since the column is actually displaying and updating the category of each email group. Changing the accessorKey to "category" improves readability and maintains consistency.

Apply this diff to fix the accessorKey:

{
-   accessorKey: "date",
+   accessorKey: "category",
    cell: ({ row }) => (
      <Select
        defaultValue={row.original.category?.id.toString() || ""}
        onValueChange={async (value) => {
          const result = await changeSenderCategoryAction({
            sender: row.original.address,
            categoryId: value,
          });
          if (isActionError(result)) {
            toastError({ description: result.error });
          } else {
            toastSuccess({ description: "Category changed" });
          }
        }}
      >
        <SelectTrigger className="w-[180px]">
          <SelectValue placeholder="Select category" />
        </SelectTrigger>
        <SelectContent>
          {categories.map((category) => (
            <SelectItem key={category.id} value={category.id.toString()}>
              {category.name}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    ),
  },
📝 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.

        accessorKey: "category",
        cell: ({ row }) => (
          <Select
            defaultValue={row.original.category?.id.toString() || ""}
            onValueChange={async (value) => {
              const result = await changeSenderCategoryAction({
                sender: row.original.address,
                categoryId: value,
              });

              if (isActionError(result)) {
                toastError({ description: result.error });
              } else {
                toastSuccess({ description: "Category changed" });
              }
            }}
          >
            <SelectTrigger className="w-[180px]">
              <SelectValue placeholder="Select category" />
            </SelectTrigger>
            <SelectContent>
              {categories.map((category) => (
                <SelectItem key={category.id} value={category.id.toString()}>
                  {category.name}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        ),
      },
    ],

389-404: ⚠️ Potential issue

Add safety checks when accessing 'thread.messages[0]'

In the ExpandedRows component, you're accessing thread.messages[0] without verifying that thread.messages is not empty. Although threads typically contain messages, adding a check ensures robustness and prevents potential runtime errors if thread.messages is empty.

Apply this diff to add safety checks:

<TableRow key={thread.id} className="bg-muted/50">
  <TableCell />
  <TableCell>
    <Link
      href={getGmailUrl(thread.id, sender)}
      target="_blank"
      className="hover:underline"
    >
-     {thread.messages[0].headers.subject}
+     {thread.messages[0]?.headers.subject || 'No Subject'}
    </Link>
  </TableCell>
- <TableCell>{decodeSnippet(thread.messages[0].snippet)}</TableCell>
+ <TableCell>{decodeSnippet(thread.messages[0]?.snippet || '')}</TableCell>
  <TableCell className="text-nowrap">
-   {formatShortDate(new Date(thread.messages[0].headers.date))}
+   {thread.messages[0]?.headers.date
+     ? formatShortDate(new Date(thread.messages[0].headers.date))
+     : 'No Date'}
  </TableCell>
</TableRow>
📝 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.

        <TableRow key={thread.id} className="bg-muted/50">
          <TableCell />
          <TableCell>
            <Link
              href={getGmailUrl(thread.id, sender)}
              target="_blank"
              className="hover:underline"
            >
              {thread.messages[0]?.headers.subject || 'No Subject'}
            </Link>
          </TableCell>
          <TableCell>{decodeSnippet(thread.messages[0]?.snippet || '')}</TableCell>
          <TableCell className="text-nowrap">
            {thread.messages[0]?.headers.date
              ? formatShortDate(new Date(thread.messages[0].headers.date))
              : 'No Date'}
          </TableCell>
        </TableRow>
apps/web/utils/actions/categorize.ts (6)

87-88: 🛠️ Refactor suggestion

Remove console.log statements to clean up production code

The console.log statement at line 87 is likely for debugging purposes. Consider removing it or replacing it with a proper logging mechanism for production.

Apply this diff to remove the debug statement:

     export const categorizeSendersAction = withActionInstrumentation(
       "categorizeSenders",
       async () => {
-        console.log("categorizeSendersAction");
📝 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.



398-399: ⚠️ Potential issue

Correct condition in personal email domain check

The condition at line 398 is checking for a > character, which might be a typo.

Apply this diff to fix the typo:

-        if (personalEmailDomains.some((domain) => sender.includes(`@${domain}>`)))
+        if (personalEmailDomains.some((domain) => sender.includes(`@${domain}`)))
📝 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.

    if (personalEmailDomains.some((domain) => sender.includes(`@${domain}`)))
      return { sender, category: defaultCategory.UNKNOWN.name };

398-399: ⚠️ Potential issue

Fix condition in personal email domain check

The condition at line 398 includes an extraneous > character, which may cause incorrect matching of email domains.

Apply this diff to correct the condition:

         if (personalEmailDomains.some((domain) => sender.includes(`@${domain}>`)))
+        if (personalEmailDomains.some((domain) => sender.includes(`@${domain}`)))
             return { sender, category: defaultCategory.UNKNOWN.name };

Committable suggestion skipped: line range outside the PR's diff.


508-512: 🛠️ Refactor suggestion

Enhance error handling in catch block

In the catch block starting at line 508, only duplicate name errors are specifically handled. Other errors are rethrown, which may not provide sufficient feedback to the user.

Consider logging unexpected errors and returning a user-friendly message:

         } catch (error) {
             if (isDuplicateError(error, "name"))
                 return { error: "Category with this name already exists" };
-            throw error;
+            console.error("Error updating category:", error);
+            return { error: "An unexpected error occurred while updating the category" };
         }
📝 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.

    if (isDuplicateError(error, "name"))
      return { error: "Category with this name already exists" };

    console.error("Error updating category:", error);
    return { error: "An unexpected error occurred while updating the category" };
  }

125-132: 🛠️ Refactor suggestion

Eliminate unnecessary console logs

Several console.log statements are present at lines 125, 127, and 131. These may clutter the output and are not suitable for production code.

Apply this diff to remove them:

     console.log("sendersResult", Array.from(sendersResult.senders.keys()));

-    console.log("sendersResult", Array.from(sendersResult.senders.keys()));

-    console.log(`Found ${sendersResult.senders.size} senders`);

     const senders = uniq(Array.from(sendersResult.senders.keys()));

-    console.log(`Found ${senders.length} unique senders`);

Committable suggestion skipped: line range outside the PR's diff.


417-419: ⚠️ Potential issue

Ensure correct usage of Prisma findUnique method

At line 417, the findUnique query uses both id and userId in the where clause. Confirm that this matches a unique constraint or consider using findFirst if appropriate.

You may need to adjust the query:

-        where: { id: categoryId, userId: session.user.id },
+        where: { id: categoryId },

Alternatively, if id is not unique globally, ensure that your schema reflects a composite unique constraint on id and userId.

Committable suggestion skipped: line range outside the PR's diff.

apps/web/app/(app)/smart-categories/Uncategorized.tsx (2)

182-184: ⚠️ Potential issue

Add error handling for setAutoCategorizeAction failures

The onChange handler in AutoCategorizeToggle assumes that setAutoCategorizeAction will always succeed. Network issues or server errors could cause this action to fail silently, leaving the user unaware of the failure. Consider adding error handling to notify the user if the action does not complete successfully.

Apply this diff to handle potential errors:

onChange={async (enabled) => {
+ try {
    await setAutoCategorizeAction(enabled);
+ } catch (error) {
+   toastError({ description: "Failed to update auto-categorize setting." });
+ }
}}
📝 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.

      onChange={async (enabled) => {
        try {
          await setAutoCategorizeAction(enabled);
        } catch (error) {
          toastError({ description: "Failed to update auto-categorize setting." });
        }
      }}

41-47: ⚠️ Potential issue

Add error handling for data fetching in useSenders

The useSWRInfinite hook currently doesn't handle errors when fetching data. If an API call fails, the user won't be notified, potentially leading to confusion. Implement error handling to alert the user in case of data fetching failures.

Apply this diff to handle errors:

const { data, size, setSize, isLoading, error } =
  useSWRInfinite<UncategorizedSendersResponse>(getKey, {
    revalidateOnFocus: false,
    revalidateFirstPage: false,
    persistSize: true,
    revalidateOnMount: true,
  });

+ useEffect(() => {
+   if (error) {
+     toastError({ description: "Failed to load senders. Please try again later." });
+   }
+ }, [error]);

Ensure you import useEffect from React:

import { useMemo, useCallback, useEffect } from "react";

Committable suggestion skipped: line range outside the PR's diff.

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