diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ExamplesList.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ExamplesList.tsx index 523ff8eaca..d7757cbc75 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/ExamplesList.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/ExamplesList.tsx @@ -71,7 +71,7 @@ function getActionType(example: string): { if (lowerExample.includes("spam") || lowerExample.includes("mark")) { return { type: "mark", color }; } - if (lowerExample.includes("label")) { + if (lowerExample.includes("label") || lowerExample.includes("categorize")) { return { type: "label", color }; } diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptFormat.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptFormat.tsx new file mode 100644 index 0000000000..b9af35f079 --- /dev/null +++ b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPromptFormat.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useCallback, useRef, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { + SimpleRichTextEditor, + type SimpleRichTextEditorRef, +} from "@/components/editor/SimpleRichTextEditor"; +import { LoadingContent } from "@/components/LoadingContent"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useLabels } from "@/hooks/useLabels"; +import { useRules } from "@/hooks/useRules"; +import { toastError } from "@/components/Toast"; +import { ruleToText } from "@/utils/rule/rule-to-text"; +import { MessageText } from "@/components/Typography"; +import { Notice } from "@/components/Notice"; + +export function RulesPromptFormat() { + const { data: rules, isLoading: isLoadingRules } = useRules(); + const { userLabels, isLoading: isLoadingLabels } = useLabels(); + + const editorRef = useRef(null); + + const rulesText = useMemo(() => { + if (!rules) return ""; + + return rules + .map((rule, index) => { + const ruleText = ruleToText(rule); + return `## Rule ${index + 1}: ${rule.name}\n${rule.enabled ? "" : "(Disabled)\n"}${ruleText}`; + }) + .join("\n\n---\n\n"); + }, [rules]); + + const onSubmit = useCallback(async () => { + const markdown = editorRef.current?.getMarkdown(); + if (typeof markdown !== "string") return; + if (markdown.trim() === "") { + toastError({ + description: "Please enter a prompt to create rules", + }); + return; + } + + // setIsSubmitting(true); + }, []); + + return ( +
{ + e.preventDefault(); + onSubmit(); + }} + > + } + > + + Editing in 'Prompt' view is currently disabled. Edit using AI Chat or + 'List' view instead. + + + + + +
+ + + + Editing in 'Prompt' view is currently disabled. Edit using AI chat or + 'List' view instead. + +
+
+ ); +} diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RulesTabNew.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RulesTabNew.tsx index aad6bf7375..8f5ac2b2c5 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/RulesTabNew.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/RulesTabNew.tsx @@ -1,13 +1,31 @@ import { Rules } from "@/app/(app)/[emailAccountId]/assistant/Rules"; +import { RulesPromptFormat } from "@/app/(app)/[emailAccountId]/assistant/RulesPromptFormat"; import { RulesPrompt } from "@/app/(app)/[emailAccountId]/assistant/RulesPromptNew"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export function RulesTab() { return (
-

Rules

- + +
+

Rules

+ + + List + Prompt + +
+ + + + + + + + +
); } diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/constants.ts b/apps/web/app/(app)/[emailAccountId]/assistant/constants.ts index 89f5d0a1c5..5b3557e5b2 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/constants.ts +++ b/apps/web/app/(app)/[emailAccountId]/assistant/constants.ts @@ -81,7 +81,7 @@ export function getActionTypeColor(example: string): string { if (lowerExample.includes("mark")) { return ACTION_TYPE_COLORS[ActionType.MARK_READ]; } - if (lowerExample.includes("label")) { + if (lowerExample.includes("label") || lowerExample.includes("categorize")) { return ACTION_TYPE_COLORS[ActionType.LABEL]; } if (lowerExample.includes("digest")) { diff --git a/apps/web/components/editor/SimpleRichTextEditor.tsx b/apps/web/components/editor/SimpleRichTextEditor.tsx index 825f36c14f..b2a9a719e5 100644 --- a/apps/web/components/editor/SimpleRichTextEditor.tsx +++ b/apps/web/components/editor/SimpleRichTextEditor.tsx @@ -17,6 +17,7 @@ interface SimpleRichTextEditorProps { minHeight?: number; userLabels?: EmailLabel[]; onClearContents?: () => void; + editable?: boolean; } export interface SimpleRichTextEditorRef { @@ -36,10 +37,12 @@ export const SimpleRichTextEditor = forwardRef< minHeight = 300, userLabels, onClearContents, + editable = true, }, ref, ) => { const editor = useEditor({ + editable, extensions: [ StarterKit.configure({ italic: false, @@ -141,7 +144,9 @@ export const SimpleRichTextEditor = forwardRef<
diff --git a/apps/web/utils/rule/rule-to-text.ts b/apps/web/utils/rule/rule-to-text.ts new file mode 100644 index 0000000000..ef991c2925 --- /dev/null +++ b/apps/web/utils/rule/rule-to-text.ts @@ -0,0 +1,121 @@ +import type { Rule, Action } from "@prisma/client"; +import { + ActionType, + CategoryFilterType, + LogicalOperator, +} from "@prisma/client"; + +export interface RuleWithActions extends Rule { + actions: Action[]; + categoryFilters?: { name: string }[]; + group?: { name: string } | null; +} + +export function ruleToText(rule: RuleWithActions): string { + const conditions: string[] = []; + const actions: string[] = []; + + // Build conditions + if (rule.instructions) { + conditions.push(rule.instructions); + } + + if (rule.from) { + conditions.push(`'From' contains "${rule.from}"`); + } + + if (rule.to) { + conditions.push(`'To' contains "${rule.to}"`); + } + + if (rule.subject) { + conditions.push(`'Subject' contains "${rule.subject}"`); + } + + if (rule.body) { + conditions.push(`'Body' contains "${rule.body}"`); + } + + // if (rule.group) { + // conditions.push(`Sender is in group "${rule.group.name}"`); + // } + + if (rule.categoryFilterType && rule.categoryFilters?.length) { + const categoryNames = rule.categoryFilters.map((c) => c.name).join(", "); + if (rule.categoryFilterType === CategoryFilterType.INCLUDE) { + conditions.push(`Sender is in categories: ${categoryNames}`); + } else { + conditions.push(`Sender is NOT in categories: ${categoryNames}`); + } + } + + // Build actions + rule.actions.forEach((action) => { + switch (action.type) { + case ActionType.ARCHIVE: + actions.push("Archive"); + break; + case ActionType.LABEL: + if (action.label) { + actions.push(`Label as @[${action.label}]`); + } + break; + case ActionType.REPLY: + if (action.content) { + actions.push(`Reply with: "${action.content}"`); + } else { + actions.push("Send reply"); + } + break; + case ActionType.FORWARD: + if (action.to) { + actions.push(`Forward to ${action.to}`); + } + break; + case ActionType.SEND_EMAIL: + actions.push(`Send email${action.to ? ` to ${action.to}` : ""}`); + break; + case ActionType.DRAFT_EMAIL: + actions.push("Draft a reply"); + break; + case ActionType.MARK_SPAM: + actions.push("Mark as spam"); + break; + case ActionType.MARK_READ: + actions.push("Mark as read"); + break; + case ActionType.CALL_WEBHOOK: + if (action.url) { + actions.push(`Call webhook: ${action.url}`); + } + break; + case ActionType.DIGEST: + actions.push("Add to digest"); + break; + case ActionType.MOVE_FOLDER: + if (action.folderName) { + actions.push(`Move to folder "${action.folderName}"`); + } + break; + case ActionType.TRACK_THREAD: + // Skip this action as it's typically internal + break; + } + }); + + // Combine conditions with operator + const operator = + rule.conditionalOperator === LogicalOperator.OR ? " OR " : " AND "; + const conditionText = + conditions.length > 0 + ? conditions.join(operator) + : "No conditions specified"; + + // Format the output with actions as bullet list + const actionsText = + actions.length > 0 + ? actions.map((action) => `- ${action}`).join("\n") + : "- No actions specified"; + + return `**When:**\n\n${conditionText}\n\n**Then:**\n${actionsText}`; +} diff --git a/version.txt b/version.txt index 4fb4686611..8bfc76e4ee 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.6.11 \ No newline at end of file +v2.6.12 \ No newline at end of file