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
19 changes: 19 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/assistant/AddRuleDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PlusIcon } from "lucide-react";
import { RulesPrompt } from "@/app/(app)/[emailAccountId]/assistant/RulesPromptNew";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function AddRuleDialog() {
return (
<Dialog>
<DialogTrigger>
<Button size="sm" Icon={PlusIcon}>
Add Rule
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl">
<RulesPrompt />
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ActionType } from "@prisma/client";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing "use client" — this component uses hooks

This file calls useAccount(); it must be a Client Component in Next.js app router.

+ "use client";
+
 import { ActionType } from "@prisma/client";

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

🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/AvailableActionsPanel.tsx at
line 1, the component uses React hooks (useAccount) but is missing the Next.js
Client Component directive; add the string "use client" as the very first line
of the file (before any imports) so the component is treated as a Client
Component, then keep the existing imports and code unchanged.

import { Card, CardContent } from "@/components/ui/card";
import { getActionIcon } from "@/utils/action-display";
import { SectionHeader } from "@/components/Typography";
import { useAccount } from "@/providers/EmailAccountProvider";
import {
getAvailableActions,
getExtraActions,
} from "@/utils/ai/rule/create-rule-schema";

const actionNames: Record<ActionType, string> = {
[ActionType.LABEL]: "Label",
[ActionType.MOVE_FOLDER]: "Move to folder",
[ActionType.ARCHIVE]: "Archive",
[ActionType.DRAFT_EMAIL]: "Draft replies",
[ActionType.REPLY]: "Send replies",
[ActionType.FORWARD]: "Forward",
[ActionType.MARK_READ]: "Mark as read",
[ActionType.MARK_SPAM]: "Mark as spam",
[ActionType.SEND_EMAIL]: "Send email",
[ActionType.CALL_WEBHOOK]: "Call webhook",
[ActionType.DIGEST]: "Add to digest",
[ActionType.TRACK_THREAD]: "Track thread",
};

export function AvailableActionsPanel() {
const { provider } = useAccount();
return (
<Card className="h-fit bg-slate-50 dark:bg-slate-900">
<CardContent className="pt-4">
<div className="grid gap-2">
<ActionSection
actions={getAvailableActions(provider)}
title="Available Actions"
/>
<ActionSection actions={getExtraActions()} title="Extra" />
</div>
</CardContent>
</Card>
);
}

function ActionSection({
title,
actions,
}: {
title: string;
actions: ActionType[];
}) {
return (
<div>
<SectionHeader>{title}</SectionHeader>
<div className="grid gap-2 mt-1">
{actions.map((actionType) => {
const Icon = getActionIcon(actionType);
return (
<div key={actionType} className="flex items-center gap-2">
<Icon className="size-3.5 text-muted-foreground" />
<span className="text-sm">{actionNames[actionType]}</span>
</div>
);
})}
</div>
</div>
);
}
114 changes: 96 additions & 18 deletions apps/web/app/(app)/[emailAccountId]/assistant/ExamplesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { memo } from "react";
import { convertLabelsToDisplay } from "@/utils/mention";
import { SectionHeader } from "@/components/Typography";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getActionTypeColor } from "@/app/(app)/[emailAccountId]/assistant/constants";
import { Button } from "@/components/ui/button";
import { getExamplePrompts } from "@/app/(app)/[emailAccountId]/assistant/examples";
import { getActionIcon } from "@/utils/action-display";
import { getActionColor } from "@/components/PlanBadge";
import { ActionType } from "@prisma/client";
import type { Color } from "@/components/Badge";
import { cn } from "@/utils";

function PureExamples({
examples,
Expand All @@ -26,7 +30,9 @@ function PureExamples({
<ScrollArea className={className}>
<div className="grid grid-cols-1 gap-2">
{examplePrompts.map((example) => {
const { color } = getActionType(example);
const actionType = getActionType(example);
const Icon = actionType ? getActionIcon(actionType) : null;
const color = actionType ? getActionColor(actionType) : "gray";

return (
<Button
Expand All @@ -36,9 +42,14 @@ function PureExamples({
className="h-auto w-full justify-start text-wrap py-2 text-left"
>
<div className="flex w-full items-start gap-2">
<div
className={`h-2 w-2 rounded-full ${color} mt-1.5 flex-shrink-0`}
/>
{Icon && (
<Icon
className={cn(
"h-4 w-4 mt-0.5 flex-shrink-0",
getIconColorClass(color),
)}
/>
)}
<span className="flex-1">
{convertLabelsToDisplay(example)}
</span>
Expand All @@ -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 (
<div className="grid grid-cols-2 gap-4">
{examplePrompts.map((example) => {
const actionType = getActionType(example);
const Icon = actionType ? getActionIcon(actionType) : null;
const color = actionType ? getActionColor(actionType) : "gray";

return (
<Button
key={example}
variant="outline"
onClick={() => onSelect(example)}
className="h-auto w-full justify-start text-wrap py-2 text-left"
>
<div className="flex w-full items-start gap-2">
{Icon && (
<Icon
className={cn(
"h-4 w-4 mt-0.5 flex-shrink-0",
getIconColorClass(color),
)}
/>
)}
<span className="flex-1">{convertLabelsToDisplay(example)}</span>
</div>
</Button>
);
})}
</div>
);
}

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";
}
}
16 changes: 13 additions & 3 deletions apps/web/app/(app)/[emailAccountId]/assistant/RuleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,11 +80,19 @@ export function RuleDialog({
<RuleForm
rule={{
name: "",
actions: [],
conditions: [],
conditions: [
{
type: ConditionType.AI,
},
],
actions: [
{
type: ActionType.LABEL,
},
],
automate: true,
runOnThreads: true,
conditionalOperator: "AND" as const,
conditionalOperator: LogicalOperator.AND,
...initialRule,
}}
alwaysEditMode={true}
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ export function RuleForm({
}
)?.instructions
}
placeholder='e.g. Apply this rule to all "receipts"'
placeholder="e.g. Newsletters, regular content from publications, blogs, or services I've subscribed to"
tooltipText="The instructions that will be passed to the AI."
/>
)}
Expand Down Expand Up @@ -1221,8 +1221,8 @@ function ActionCard({
!setManually ? (
<div className="mt-2 flex h-full flex-col items-center justify-center gap-2 p-4 border rounded">
<div className="max-w-sm text-center text-sm text-muted-foreground">
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
</div>

<Button
Expand Down
Loading
Loading