-
+ {Icon && (
+
+ )}
{convertLabelsToDisplay(example)}
@@ -54,28 +65,95 @@ function PureExamples({
export const Examples = memo(PureExamples);
-function getActionType(example: string): {
- type: string;
- color: string;
-} {
+function PureExamplesGrid({
+ examples,
+ onSelect,
+ provider,
+}: {
+ examples: string[];
+ onSelect: (example: string) => void;
+ provider: string;
+ className?: string;
+}) {
+ const examplePrompts = getExamplePrompts(provider, examples);
+
+ return (
+
+ {examplePrompts.map((example) => {
+ const actionType = getActionType(example);
+ const Icon = actionType ? getActionIcon(actionType) : null;
+ const color = actionType ? getActionColor(actionType) : "gray";
+
+ return (
+
+ );
+ })}
+
+ );
+}
+
+export const ExamplesGrid = memo(PureExamplesGrid);
+
+function getActionType(example: string): ActionType | null {
const lowerExample = example.toLowerCase();
- const color = getActionTypeColor(example);
if (lowerExample.includes("forward")) {
- return { type: "forward", color };
+ return ActionType.FORWARD;
}
- if (lowerExample.includes("draft") || lowerExample.includes("reply")) {
- return { type: "reply", color };
+ if (lowerExample.includes("draft")) {
+ return ActionType.DRAFT_EMAIL;
+ }
+ if (lowerExample.includes("reply")) {
+ return ActionType.REPLY;
}
if (lowerExample.includes("archive")) {
- return { type: "archive", color };
+ return ActionType.ARCHIVE;
+ }
+ if (lowerExample.includes("spam")) {
+ return ActionType.MARK_SPAM;
}
- if (lowerExample.includes("spam") || lowerExample.includes("mark")) {
- return { type: "mark", color };
+ if (lowerExample.includes("mark")) {
+ return ActionType.MARK_READ;
}
if (lowerExample.includes("label") || lowerExample.includes("categorize")) {
- return { type: "label", color };
+ return ActionType.LABEL;
}
- return { type: "other", color };
+ return null;
+}
+
+function getIconColorClass(color: Color): string {
+ switch (color) {
+ case "green":
+ return "text-green-600 dark:text-green-400";
+ case "yellow":
+ return "text-yellow-600 dark:text-yellow-400";
+ case "blue":
+ return "text-blue-600 dark:text-blue-400";
+ case "red":
+ return "text-red-600 dark:text-red-400";
+ case "purple":
+ return "text-purple-600 dark:text-purple-400";
+ case "indigo":
+ return "text-indigo-600 dark:text-indigo-400";
+ default:
+ return "text-gray-600 dark:text-gray-400";
+ }
}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RuleDialog.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RuleDialog.tsx
index 0e15f26ed2..915f1676e2 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/RuleDialog.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/RuleDialog.tsx
@@ -12,6 +12,8 @@ import { LoadingContent } from "@/components/LoadingContent";
import { useRule } from "@/hooks/useRule";
import type { CreateRuleBody } from "@/utils/actions/rule.validation";
import { useDialogState } from "@/hooks/useDialogState";
+import { ActionType, LogicalOperator } from "@prisma/client";
+import { ConditionType } from "@/utils/config";
interface RuleDialogProps {
ruleId?: string;
@@ -78,11 +80,19 @@ export function RuleDialog({
)}
@@ -1221,8 +1221,8 @@ function ActionCard({
!setManually ? (
- Our AI will generate a reply using your knowledge base
- and previous conversations with the sender
+ Our AI will generate a reply based on your email history
+ and knowledge base
-
- {
- if (isColdEmailBlocker) {
- coldEmailDialog.onOpen();
- } else {
- ruleDialog.onOpen({
- ruleId: rule.id,
- editMode: false,
- });
- }
- }}
- >
-
- View
-
+ e.stopPropagation()}
+ >
{
if (isColdEmailBlocker) {
@@ -523,17 +495,20 @@ export function ActionBadges({
provider: string;
}) {
return (
-
+
{sortActionsByPriority(actions).map((action) => {
// Hidden for simplicity
if (action.type === ActionType.TRACK_THREAD) return null;
+ const Icon = getActionIcon(action.type);
+
return (
+
{getActionDisplay(action, provider)}
);
@@ -549,14 +524,3 @@ function NoRules() {
);
}
-
-function AddRuleButton({ onClick }: { onClick: () => void }) {
- return (
-
- );
-}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptNew.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptNew.tsx
index 038d9fa9b4..2ff8fa7e6d 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptNew.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptNew.tsx
@@ -22,11 +22,12 @@ import { useLabels } from "@/hooks/useLabels";
import { RuleDialog } from "@/app/(app)/[emailAccountId]/assistant/RuleDialog";
import { useDialogState } from "@/hooks/useDialogState";
import { useRules } from "@/hooks/useRules";
-import { Examples } from "@/app/(app)/[emailAccountId]/assistant/ExamplesList";
+import { ExamplesGrid } from "@/app/(app)/[emailAccountId]/assistant/ExamplesList";
import { AssistantOnboarding } from "@/app/(app)/[emailAccountId]/assistant/AssistantOnboarding";
import { CreatedRulesModal } from "@/app/(app)/[emailAccountId]/assistant/CreatedRulesModal";
import type { CreateRuleResult } from "@/utils/rule/types";
import { toastError } from "@/components/Toast";
+import { AvailableActionsPanel } from "@/app/(app)/[emailAccountId]/assistant/AvailableActionsPanel";
export function RulesPrompt() {
const { emailAccountId, provider } = useAccount();
@@ -50,6 +51,7 @@ export function RulesPrompt() {
provider={provider}
examples={examples}
onOpenPersonaDialog={onOpenPersonaDialog}
+ onHideExamples={() => setPersona(null)}
/>
void;
+ onHideExamples: () => void;
}) {
const { mutate } = useRules();
const { userLabels, isLoading: isLoadingLabels } = useLabels();
@@ -146,114 +150,75 @@ function RulesPromptForm({
return (
-
- {examples && (
-
- )}
-
-
+
+
+
+
);
}
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts b/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts
index ed88ab4f09..f6a5f4a1be 100644
--- a/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts
+++ b/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts
@@ -43,8 +43,8 @@ function processPromptsWithTerminology(
}
const commonPrompts = [
- "Label urgent emails as @[Urgent]",
"Label emails from @mycompany.com addresses as @[Team]",
+ "Label urgent emails as @[Urgent]",
];
const examplePromptsBase = [
diff --git a/apps/web/app/(app)/[emailAccountId]/automation/page.tsx b/apps/web/app/(app)/[emailAccountId]/automation/page.tsx
index 4c3f5a8e09..cf774837f9 100644
--- a/apps/web/app/(app)/[emailAccountId]/automation/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/automation/page.tsx
@@ -124,18 +124,17 @@ export default async function AutomationPage({
-
- }
- title="Getting started with AI Assistant"
- description={
- "Learn how to use the AI Assistant to automatically label, archive, and more."
- }
- videoSrc="https://www.youtube.com/embed/SoeNDVr7ve4"
- thumbnailSrc="https://img.youtube.com/vi/SoeNDVr7ve4/0.jpg"
- storageKey="ai-assistant-onboarding-video"
- />
-
+ }
+ title="Getting started with AI Assistant"
+ description={
+ "Learn how to use the AI Assistant to automatically label, archive, and more."
+ }
+ videoSrc="https://www.youtube.com/embed/SoeNDVr7ve4"
+ thumbnailSrc="https://img.youtube.com/vi/SoeNDVr7ve4/0.jpg"
+ storageKey="ai-assistant-onboarding-video"
+ />
diff --git a/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx b/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx
index e88ec64a6e..775b7ab1d6 100644
--- a/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx
@@ -234,18 +234,17 @@ export function BulkUnsubscribe() {
}}
/>
-
- }
- title="Getting started with Bulk Unsubscribe"
- description={
- "Learn how to use the Bulk Unsubscribe to unsubscribe from and archive unwanted emails."
- }
- videoSrc="https://www.youtube.com/embed/T1rnooV4OYc"
- thumbnailSrc="https://img.youtube.com/vi/T1rnooV4OYc/0.jpg"
- storageKey="bulk-unsubscribe-onboarding-video"
- />
-
+ }
+ title="Getting started with Bulk Unsubscribe"
+ description={
+ "Learn how to use the Bulk Unsubscribe to unsubscribe from and archive unwanted emails."
+ }
+ videoSrc="https://www.youtube.com/embed/T1rnooV4OYc"
+ thumbnailSrc="https://img.youtube.com/vi/T1rnooV4OYc/0.jpg"
+ storageKey="bulk-unsubscribe-onboarding-video"
+ />
diff --git a/apps/web/app/(landing)/components/page.tsx b/apps/web/app/(landing)/components/page.tsx
index b725532dfb..c8c569f7a0 100644
--- a/apps/web/app/(landing)/components/page.tsx
+++ b/apps/web/app/(landing)/components/page.tsx
@@ -28,8 +28,11 @@ import {
import { TooltipExplanation } from "@/components/TooltipExplanation";
import { Suspense } from "react";
import { PremiumAiAssistantAlert } from "@/components/PremiumAlert";
-import { PremiumTier } from "@prisma/client";
+import { ActionType, PremiumTier } from "@prisma/client";
import { SettingCard } from "@/components/SettingCard";
+import { IconCircle } from "@/app/(app)/[emailAccountId]/onboarding/IconCircle";
+import { ActionBadges } from "@/app/(app)/[emailAccountId]/assistant/Rules";
+import { DismissibleVideoCard } from "@/components/VideoCard";
export const maxDuration = 3;
@@ -219,6 +222,107 @@ export default function Components() {
+
+
DismissibleVideoCard
+
+ }
+ title="Getting started with AI Assistant"
+ description={
+ "Learn how to use the AI Assistant to automatically label, archive, and more."
+ }
+ videoSrc="https://www.youtube.com/embed/SoeNDVr7ve4"
+ thumbnailSrc="https://img.youtube.com/vi/SoeNDVr7ve4/0.jpg"
+ storageKey={`video-dismissible-${Date.now()}`}
+ />
+
+
+
+
+
+
+
MultiSelectFilter
diff --git a/apps/web/components/ExpandableText.tsx b/apps/web/components/ExpandableText.tsx
index 204229eb33..28c9b34123 100644
--- a/apps/web/components/ExpandableText.tsx
+++ b/apps/web/components/ExpandableText.tsx
@@ -42,7 +42,10 @@ export function ExpandableText({
setIsExpanded(!isExpanded)}
+ onClick={(e) => {
+ e.stopPropagation();
+ setIsExpanded(!isExpanded);
+ }}
className="mt-1 flex items-center text-xs text-muted-foreground hover:text-primary"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
diff --git a/apps/web/components/PlanBadge.tsx b/apps/web/components/PlanBadge.tsx
index c128f869c9..c63877c870 100644
--- a/apps/web/components/PlanBadge.tsx
+++ b/apps/web/components/PlanBadge.tsx
@@ -214,9 +214,18 @@ export function getActionColor(actionType: ActionType): Color {
case ActionType.MARK_READ:
return "yellow";
case ActionType.LABEL:
+ case ActionType.MOVE_FOLDER:
return "blue";
- default:
+ case ActionType.MARK_SPAM:
+ return "red";
+ case ActionType.CALL_WEBHOOK:
+ case ActionType.TRACK_THREAD:
+ case ActionType.DIGEST:
return "purple";
+ default: {
+ const exhaustiveCheck: never = actionType;
+ return exhaustiveCheck;
+ }
}
}
diff --git a/apps/web/components/PremiumAlert.tsx b/apps/web/components/PremiumAlert.tsx
index 0d523ea54e..76185639cf 100644
--- a/apps/web/components/PremiumAlert.tsx
+++ b/apps/web/components/PremiumAlert.tsx
@@ -81,7 +81,11 @@ export function PremiumAiAssistantAlert({
icon={}
title={`${businessTierName} Plan Required`}
description={`Switch to the ${businessTierName} plan to use this feature.`}
- action={Switch Plan}
+ action={
+
+ Switch Plan
+
+ }
/>
) : showSetApiKey ? (
+
Set API Key
}
@@ -99,7 +103,11 @@ export function PremiumAiAssistantAlert({
icon={}
title="Premium Feature"
description={`This is a premium feature. Upgrade to the ${businessTierName} plan.`}
- action={Upgrade}
+ action={
+
+ Upgrade
+
+ }
/>
)}
diff --git a/apps/web/utils/action-display.ts b/apps/web/utils/action-display.tsx
similarity index 53%
rename from apps/web/utils/action-display.ts
rename to apps/web/utils/action-display.tsx
index 029c03c6ea..b9b9e855f1 100644
--- a/apps/web/utils/action-display.ts
+++ b/apps/web/utils/action-display.tsx
@@ -1,6 +1,20 @@
import { capitalCase } from "capital-case";
import { ActionType } from "@prisma/client";
import { getEmailTerminology } from "@/utils/terminology";
+import {
+ ArchiveIcon,
+ FolderInputIcon,
+ ForwardIcon,
+ ReplyIcon,
+ ShieldCheckIcon,
+ SendIcon,
+ TagIcon,
+ WebhookIcon,
+ FileTextIcon,
+ EyeIcon,
+ MailIcon,
+ NewspaperIcon,
+} from "lucide-react";
export function getActionDisplay(
action: {
@@ -21,7 +35,7 @@ export function getActionDisplay(
return "Draft Reply";
case ActionType.LABEL:
return action.label
- ? `${terminology.label.action}: ${action.label}`
+ ? `${terminology.label.action} as '${action.label}'`
: terminology.label.action;
case ActionType.ARCHIVE:
return "Skip Inbox";
@@ -37,10 +51,43 @@ export function getActionDisplay(
return `Auto-update reply ${terminology.label.singular}`;
case ActionType.MOVE_FOLDER:
return action.folderName
- ? `Folder: ${action.folderName}`
+ ? `Move to '${action.folderName}' folder`
: "Move to folder";
default:
// Default to capital case for other action types
return capitalCase(action.type);
}
}
+
+export function getActionIcon(actionType: ActionType) {
+ switch (actionType) {
+ case ActionType.LABEL:
+ return TagIcon;
+ case ActionType.ARCHIVE:
+ return ArchiveIcon;
+ case ActionType.MOVE_FOLDER:
+ return FolderInputIcon;
+ case ActionType.DRAFT_EMAIL:
+ return FileTextIcon;
+ case ActionType.REPLY:
+ return ReplyIcon;
+ case ActionType.SEND_EMAIL:
+ return SendIcon;
+ case ActionType.FORWARD:
+ return ForwardIcon;
+ case ActionType.MARK_SPAM:
+ return ShieldCheckIcon;
+ case ActionType.MARK_READ:
+ return MailIcon;
+ case ActionType.CALL_WEBHOOK:
+ return WebhookIcon;
+ case ActionType.TRACK_THREAD:
+ return EyeIcon;
+ case ActionType.DIGEST:
+ return NewspaperIcon;
+ default: {
+ const exhaustiveCheck: never = actionType;
+ return exhaustiveCheck;
+ }
+ }
+}
diff --git a/apps/web/utils/action-sort.ts b/apps/web/utils/action-sort.ts
index d196529731..310572bcde 100644
--- a/apps/web/utils/action-sort.ts
+++ b/apps/web/utils/action-sort.ts
@@ -8,19 +8,19 @@ import { ActionType } from "@prisma/client";
const ACTION_TYPE_PRIORITY_ORDER: ActionType[] = [
ActionType.LABEL,
- ActionType.ARCHIVE,
ActionType.MOVE_FOLDER,
-
+ ActionType.ARCHIVE,
ActionType.MARK_READ,
- ActionType.MARK_SPAM,
ActionType.DRAFT_EMAIL,
ActionType.REPLY,
ActionType.SEND_EMAIL,
ActionType.FORWARD,
- ActionType.CALL_WEBHOOK,
ActionType.DIGEST,
+
+ ActionType.MARK_SPAM,
+ ActionType.CALL_WEBHOOK,
ActionType.TRACK_THREAD,
];
diff --git a/apps/web/utils/ai/rule/create-rule-schema.ts b/apps/web/utils/ai/rule/create-rule-schema.ts
index 2026103fdf..1d1b1f2490 100644
--- a/apps/web/utils/ai/rule/create-rule-schema.ts
+++ b/apps/web/utils/ai/rule/create-rule-schema.ts
@@ -6,6 +6,7 @@ import {
} from "@prisma/client";
import { delayInMinutesSchema } from "@/utils/actions/rule.validation";
import { isMicrosoftProvider } from "@/utils/email/provider-types";
+import { isDefined } from "@/utils/types";
const conditionSchema = z
.object({
@@ -34,17 +35,32 @@ const conditionSchema = z
})
.describe("The conditions to match");
+export function getAvailableActions(provider: string) {
+ const availableActions: ActionType[] = [
+ ActionType.LABEL,
+ ...(isMicrosoftProvider(provider) ? [ActionType.MOVE_FOLDER] : []),
+ ActionType.ARCHIVE,
+ ActionType.MARK_READ,
+ ActionType.DRAFT_EMAIL,
+ ActionType.REPLY,
+ ActionType.FORWARD,
+ ActionType.MARK_SPAM,
+ ].filter(isDefined);
+ return availableActions as [ActionType, ...ActionType[]];
+}
+
+export const getExtraActions = () => [
+ ActionType.DIGEST,
+ ActionType.CALL_WEBHOOK,
+];
+
const actionSchema = (provider: string) =>
z.object({
type: z
- .enum(
- isMicrosoftProvider(provider)
- ? (Object.values(ActionType) as [ActionType, ...ActionType[]])
- : (Object.values(ActionType).filter(
- (type) => type !== ActionType.MOVE_FOLDER,
- ) as [ActionType, ...ActionType[]]),
- )
- .describe("The type of the action"),
+ .enum([...getAvailableActions(provider), ...getExtraActions()])
+ .describe(
+ `The type of the action. '${ActionType.DIGEST}' means emails will be added to the digest email the user receives. ${isMicrosoftProvider(provider) ? `'${ActionType.LABEL}' means emails will be categorized in Outlook.` : ""}`,
+ ),
fields: z
.object({
label: z
diff --git a/version.txt b/version.txt
index 5114997f44..c0151c8a56 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v2.9.48
+v2.10.1