Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import Link from "next/link";
import { SettingsIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ActionCard } from "@/components/ui/card";
import { useRules } from "@/hooks/useRules";
import { useAccount } from "@/providers/EmailAccountProvider";
import { prefixPath } from "@/utils/path";
import {
STEP_KEYS,
getStepNumber,
} from "@/app/(app)/[emailAccountId]/onboarding/steps";

export function AllRulesDisabledBanner() {
const { data: rules, isLoading } = useRules();
const { emailAccountId } = useAccount();

if (isLoading || !rules) return null;

const allRulesDisabled =
rules.length > 0 && rules.every((rule) => !rule.enabled);

if (!allRulesDisabled) return null;

return (
<ActionCard
className="max-w-full mt-4"
variant="blue"
icon={<SettingsIcon className="h-5 w-5" />}
title="All rules are disabled"
description="Your AI Assistant isn't processing emails because all rules are disabled. Enable them to get started."
action={
<Button asChild variant="primaryBlack">
<Link
href={prefixPath(
emailAccountId,
`/onboarding?step=${getStepNumber(STEP_KEYS.LABELS)}&force=true`,
)}
>
Set up rules
</Link>
</Button>
}
/>
);
}
3 changes: 3 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/automation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SettingsTab } from "@/app/(app)/[emailAccountId]/assistant/settings/Set
import { TabSelect } from "@/components/TabSelect";
import { RulesTab } from "@/app/(app)/[emailAccountId]/assistant/RulesTabNew";
import { AIChatButton } from "@/app/(app)/[emailAccountId]/assistant/AIChatButton";
import { AllRulesDisabledBanner } from "@/app/(app)/[emailAccountId]/assistant/AllRulesDisabledBanner";
import { PageWrapper } from "@/components/PageWrapper";
import { PageHeader } from "@/components/PageHeader";
import { DismissibleVideoCard } from "@/components/VideoCard";
Expand Down Expand Up @@ -104,6 +105,8 @@ export default async function AutomationPage({
</div>
</div>

<AllRulesDisabledBanner />

<div className="border-b border-neutral-200 pt-2">
<TabSelect
options={tabOptions(emailAccountId)}
Expand Down
19 changes: 14 additions & 5 deletions apps/web/app/(app)/[emailAccountId]/clean/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from "react";
import { Card, CardTitle } from "@/components/ui/card";
import { IntroStep } from "@/app/(app)/[emailAccountId]/clean/IntroStep";
import { ActionSelectionStep } from "@/app/(app)/[emailAccountId]/clean/ActionSelectionStep";
Expand Down Expand Up @@ -27,6 +28,8 @@ export default async function CleanPage(props: {
}>;
}) {
const { emailAccountId } = await props.params;
const searchParams = await props.searchParams;

await checkUserOwnsEmailAccount({ emailAccountId });

const emailAccount = await prisma.emailAccount.findUnique({
Expand All @@ -47,10 +50,7 @@ export default async function CleanPage(props: {
});
const { unhandledCount } = await getUnhandledCount(emailProvider);

const searchParams = await props.searchParams;
const step = searchParams.step
? Number.parseInt(searchParams.step)
: CleanStep.INTRO;
const step = Number.parseInt(searchParams.step || "") || CleanStep.INTRO;

const renderStepContent = () => {
switch (step) {
Expand Down Expand Up @@ -96,7 +96,16 @@ export default async function CleanPage(props: {
return (
<div>
<Card className="my-4 max-w-2xl p-6 sm:mx-4 md:mx-auto">
{renderStepContent()}
<Suspense
key={step}
fallback={
<div className="flex h-[400px] items-center justify-center">
Loading...
</div>
}
>
{renderStepContent()}
</Suspense>
</Card>
</div>
);
Expand Down
23 changes: 23 additions & 0 deletions apps/web/app/(landing)/components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Button as ShadButton } from "@/components/ui/button";
import { Badge } from "@/components/Badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Table, TableBody, TableRow, TableCell } from "@/components/ui/table";
import { ActionCard } from "@/components/ui/card";
import { AlertBasic } from "@/components/Alert";
import { Notice } from "@/components/Notice";
import { TestErrorButton } from "@/app/(landing)/components/TestError";
Expand Down Expand Up @@ -72,6 +73,28 @@ export default function Components() {
<div className="space-y-6">
<div className="underline">Card</div>
<CardBasic>This is a basic card.</CardBasic>
<div className="space-y-4">
<ActionCard
icon={<SparklesIcon className="size-5" />}
title="Action Card (Green)"
description="This is the default green variant of the ActionCard component."
action={<ShadButton variant="primaryBlack">Click Me</ShadButton>}
/>
<ActionCard
variant="blue"
icon={<SparklesIcon className="size-5" />}
title="Action Card (Blue)"
description="This is the blue variant of the ActionCard component."
action={<ShadButton variant="primaryBlack">Click Me</ShadButton>}
/>
<ActionCard
variant="destructive"
icon={<SparklesIcon className="size-5" />}
title="Action Card (Destructive)"
description="This is the destructive variant of the ActionCard component."
action={<ShadButton variant="primaryBlack">Click Me</ShadButton>}
/>
</div>
</div>

<div className="space-y-6">
Expand Down
3 changes: 3 additions & 0 deletions apps/web/components/ui/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const alertVariants = cva(
},
);

/** @deprecated Use ActionCard from "@/components/ui/card" instead */
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
Expand All @@ -35,6 +36,7 @@ const Alert = React.forwardRef<
));
Alert.displayName = "Alert";

/** @deprecated Use ActionCard from "@/components/ui/card" instead */
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
Expand All @@ -47,6 +49,7 @@ const AlertTitle = React.forwardRef<
));
AlertTitle.displayName = "AlertTitle";

/** @deprecated Use ActionCard from "@/components/ui/card" instead */
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
Expand Down
97 changes: 80 additions & 17 deletions apps/web/components/ui/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,32 +106,93 @@ const CardGreen = React.forwardRef<
));
CardGreen.displayName = "CardGreen";

const CardBlue = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<Card
ref={ref}
className={cn(
"border-blue-100 bg-gradient-to-tr from-transparent via-blue-50/80 to-blue-500/15 dark:border-blue-900 dark:from-blue-950/50 dark:via-blue-900/20 dark:to-blue-800/10",
className,
)}
{...props}
/>
));
CardBlue.displayName = "CardBlue";

const CardRed = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<Card
ref={ref}
className={cn(
"border-red-100 bg-gradient-to-tr from-transparent via-red-50/80 to-red-500/15 dark:border-red-900 dark:from-red-950/50 dark:via-red-900/20 dark:to-red-800/10",
className,
)}
{...props}
/>
));
CardRed.displayName = "CardRed";

const ActionCard = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
icon?: React.ReactNode;
title: string;
description: string;
description: string | React.ReactNode;
Comment thread
elie222 marked this conversation as resolved.
action?: React.ReactNode;
variant?: "green" | "blue" | "destructive";
}
>(({ className, icon, title, description, action, ...props }, ref) => (
<CardGreen ref={ref} className={cn("max-w-2xl", className)} {...props}>
<div className="flex items-center justify-between gap-4 p-6">
<div className="flex items-start gap-3">
{icon && (
<div className="mt-0.5 flex-shrink-0 text-green-600 dark:text-green-400">
{icon}
>(
(
{
className,
icon,
title,
description,
action,
variant = "green",
...props
},
ref,
) => {
const CardVariant =
variant === "blue"
? CardBlue
: variant === "destructive"
? CardRed
: CardGreen;
const iconColor =
variant === "blue"
? "text-blue-600 dark:text-blue-400"
: variant === "destructive"
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400";

return (
<CardVariant ref={ref} className={cn("max-w-2xl", className)} {...props}>
<div className="flex items-center justify-between gap-4 p-6">
<div className="flex items-start gap-3">
{icon && (
<div className={cn("mt-0.5 flex-shrink-0", iconColor)}>
{icon}
</div>
)}
<div>
<h3 className="text-lg font-semibold">{title}</h3>
<div className="mt-1 text-sm text-muted-foreground">
{description}
</div>
</div>
</div>
)}
<div>
<h3 className="text-lg font-semibold">{title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
{action && <div className="flex-shrink-0">{action}</div>}
</div>
</div>
{action && <div className="flex-shrink-0">{action}</div>}
</div>
</CardGreen>
));
</CardVariant>
);
},
);
ActionCard.displayName = "ActionCard";

export {
Expand All @@ -143,5 +204,7 @@ export {
CardContent,
CardBasic,
CardGreen,
CardBlue,
CardRed,
ActionCard,
};
2 changes: 1 addition & 1 deletion apps/web/utils/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const updateAiSettingsAction = actionClientUser
ErrorType.INCORRECT_OPENAI_API_KEY,
ErrorType.INVALID_OPENAI_MODEL,
ErrorType.OPENAI_API_KEY_DEACTIVATED,
ErrorType.OPENAI_RETRY_ERROR,
ErrorType.AI_QUOTA_ERROR,
ErrorType.ANTHROPIC_INSUFFICIENT_BALANCE,
],
logger,
Expand Down
6 changes: 3 additions & 3 deletions apps/web/utils/error-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const ErrorType = {
INCORRECT_OPENAI_API_KEY: "Incorrect OpenAI API key",
INVALID_OPENAI_MODEL: "Invalid OpenAI model",
OPENAI_API_KEY_DEACTIVATED: "OpenAI API key deactivated",
OPENAI_RETRY_ERROR: "OpenAI retry error",
AI_QUOTA_ERROR: "AI quota error",
ANTHROPIC_INSUFFICIENT_BALANCE: "Anthropic insufficient balance",
ACCOUNT_DISCONNECTED: "Account disconnected",
};
Expand All @@ -137,8 +137,8 @@ const errorTypeConfig: Record<
actionUrl: "/settings",
actionLabel: "Update API Key",
},
[ErrorType.OPENAI_RETRY_ERROR]: {
label: "API Quota Exceeded",
[ErrorType.AI_QUOTA_ERROR]: {
label: "AI Rate Limited",
actionUrl: "/settings",
actionLabel: "Update Settings",
},
Expand Down
26 changes: 17 additions & 9 deletions apps/web/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,18 @@ export function isAnthropicInsufficientBalanceError(
);
}

// Handling OpenAI retry errors on their own because this will be related to the user's own API quota,
// rather than an error on our side (as we default to Anthropic atm).
export function isOpenAIRetryError(error: RetryError): boolean {
return error.message.includes("You exceeded your current quota");
// Handling AI quota/retry errors. This can be related to the user's own API quota or the system's quota.
export function isAiQuotaExceededError(error: RetryError): boolean {
const message = error.message.toLowerCase();
const quotaErrorMessages = [
"exceeded your current quota",
"quota exceeded",
"rate limit reached",
"rate_limit_reached",
"too many requests",
"hit a rate limit",
];
return quotaErrorMessages.some((substr) => message.includes(substr));
}

export function isAWSThrottlingError(error: unknown): error is Error {
Expand Down Expand Up @@ -178,7 +186,7 @@ export function isKnownApiError(error: unknown): boolean {
isInvalidOpenAIModelError(error) ||
isOpenAIAPIKeyDeactivatedError(error) ||
isAnthropicInsufficientBalanceError(error))) ||
(RetryError.isInstance(error) && isOpenAIRetryError(error))
(RetryError.isInstance(error) && isAiQuotaExceededError(error))
);
}

Expand Down Expand Up @@ -227,11 +235,11 @@ export function checkCommonErrors(
};
}

if (RetryError.isInstance(error) && isOpenAIRetryError(error)) {
logger.warn("OpenAI quota exceeded for url", { url });
if (RetryError.isInstance(error) && isAiQuotaExceededError(error)) {
logger.warn("AI quota exceeded for url", { url });
return {
type: "OpenAI Quota Exceeded",
message: `OpenAI error: ${error.message}`,
type: "AI Quota Exceeded",
message: `AI error: ${error.message}`,
code: 429,
};
}
Expand Down
8 changes: 4 additions & 4 deletions apps/web/utils/llms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
isIncorrectOpenAIAPIKeyError,
isInvalidOpenAIModelError,
isOpenAIAPIKeyDeactivatedError,
isOpenAIRetryError,
isAiQuotaExceededError,
isServiceUnavailableError,
} from "@/utils/error";
import { getModel, type ModelType } from "@/utils/llms/model";
Expand Down Expand Up @@ -316,14 +316,14 @@ async function handleError(
modelName,
});

if (RetryError.isInstance(error) && isOpenAIRetryError(error)) {
if (RetryError.isInstance(error) && isAiQuotaExceededError(error)) {
return await addUserErrorMessageWithNotification({
userId,
userEmail,
emailAccountId,
errorType: ErrorType.OPENAI_RETRY_ERROR,
errorType: ErrorType.AI_QUOTA_ERROR,
errorMessage:
"You have exceeded your OpenAI API quota. Please check your OpenAI account.",
"Your AI provider has rejected requests due to rate limits or quota. Please check your provider account if this persists.",
logger,
});
}
Expand Down
Loading