-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add readonly prompt view for rules #724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<SimpleRichTextEditorRef>(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 ( | ||
| <form | ||
| onSubmit={(e) => { | ||
| e.preventDefault(); | ||
| onSubmit(); | ||
| }} | ||
| > | ||
| <LoadingContent | ||
| loading={isLoadingLabels || isLoadingRules} | ||
| loadingComponent={<Skeleton className="min-h-[220px] w-full" />} | ||
| > | ||
| <Notice variant="info" className="mb-2"> | ||
| Editing in 'Prompt' view is currently disabled. Edit using AI Chat or | ||
| 'List' view instead. | ||
| </Notice> | ||
|
|
||
| <SimpleRichTextEditor | ||
| ref={editorRef} | ||
| defaultValue={rulesText} | ||
| minHeight={220} | ||
| userLabels={userLabels} | ||
| editable={false} | ||
| /> | ||
| </LoadingContent> | ||
|
|
||
| <div className="flex flex-wrap gap-2 mt-4 items-center"> | ||
| <Button type="submit" size="sm" disabled> | ||
| Save | ||
| </Button> | ||
|
|
||
| <MessageText className="pl-2"> | ||
| Editing in 'Prompt' view is currently disabled. Edit using AI chat or | ||
| 'List' view instead. | ||
| </MessageText> | ||
| </div> | ||
| </form> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div> | ||
| <RulesPrompt /> | ||
|
|
||
| <h3 className="font-cal text-xl leading-7 mt-8 mb-2">Rules</h3> | ||
| <Rules showAddRuleButton={false} /> | ||
| <Tabs defaultValue="list" searchParam="format"> | ||
| <div className="flex justify-between items-center mt-8 mb-2"> | ||
| <h3 className="font-cal text-xl leading-7">Rules</h3> | ||
|
|
||
| <TabsList> | ||
| <TabsTrigger value="list">List</TabsTrigger> | ||
| <TabsTrigger value="prompt">Prompt</TabsTrigger> | ||
| </TabsList> | ||
| </div> | ||
|
|
||
| <TabsContent value="list"> | ||
| <Rules showAddRuleButton={false} /> | ||
| </TabsContent> | ||
|
|
||
| <TabsContent value="prompt"> | ||
| <RulesPromptFormat /> | ||
| </TabsContent> | ||
| </Tabs> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import type { Rule, Action } from "@prisma/client"; | ||
| import { | ||
| ActionType, | ||
| CategoryFilterType, | ||
| LogicalOperator, | ||
| } from "@prisma/client"; | ||
|
Comment on lines
+2
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainDo not import Prisma enums in the web bundle; compare to string literals instead. Importing Apply: -import type { Rule, Action } from "@prisma/client";
-import {
- ActionType,
- CategoryFilterType,
- LogicalOperator,
-} from "@prisma/client";
+import type { Rule, Action } from "@prisma/client";
@@
- if (rule.categoryFilterType && rule.categoryFilters?.length) {
+ if (rule.categoryFilterType && rule.categoryFilters?.length) {
const categoryNames = rule.categoryFilters.map((c) => c.name).join(", ");
- if (rule.categoryFilterType === CategoryFilterType.INCLUDE) {
+ if (rule.categoryFilterType === "INCLUDE") {
conditions.push(`Sender is in categories: ${categoryNames}`);
} else {
conditions.push(`Sender is NOT in categories: ${categoryNames}`);
}
}
@@
- rule.actions.forEach((action) => {
+ rule.actions.forEach((action) => {
switch (action.type) {
- case ActionType.ARCHIVE:
+ case "ARCHIVE":
actions.push("Archive");
break;
- case ActionType.LABEL:
+ case "LABEL":
if (action.label) {
actions.push(`Label as @[${action.label}]`);
}
break;
- case ActionType.REPLY:
+ case "REPLY":
if (action.content) {
actions.push(`Reply with: "${action.content}"`);
} else {
actions.push("Send reply");
}
break;
- case ActionType.FORWARD:
+ case "FORWARD":
if (action.to) {
actions.push(`Forward to ${action.to}`);
}
break;
- case ActionType.SEND_EMAIL:
+ case "SEND_EMAIL":
actions.push(`Send email${action.to ? ` to ${action.to}` : ""}`);
break;
- case ActionType.DRAFT_EMAIL:
+ case "DRAFT_EMAIL":
actions.push("Draft a reply");
break;
- case ActionType.MARK_SPAM:
+ case "MARK_SPAM":
actions.push("Mark as spam");
break;
- case ActionType.MARK_READ:
+ case "MARK_READ":
actions.push("Mark as read");
break;
- case ActionType.CALL_WEBHOOK:
+ case "CALL_WEBHOOK":
if (action.url) {
actions.push(`Call webhook: ${action.url}`);
}
break;
- case ActionType.DIGEST:
+ case "DIGEST":
actions.push("Add to digest");
break;
- case ActionType.MOVE_FOLDER:
+ case "MOVE_FOLDER":
if (action.folderName) {
actions.push(`Move to folder "${action.folderName}"`);
}
break;
- case ActionType.TRACK_THREAD:
+ case "TRACK_THREAD":
// Skip this action as it's typically internal
break;
}
});
@@
- const operator =
- rule.conditionalOperator === LogicalOperator.OR ? " OR " : " AND ";
+ const operator = rule.conditionalOperator === "OR" ? " OR " : " AND ";Check for any other client-side imports of Also applies to: 43-50, 55-103, 107-109 🏁 Script executed: #!/bin/bash
rg -nP --glob 'apps/web/**' -C2 'from\s+"@prisma/client";'Length of output: 80559 Remove all runtime imports of 🤖 Prompt for AI Agents |
||
|
|
||
| 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}`; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| v2.6.11 | ||
| v2.6.12 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Match constants.ts: add “categorise”, “categorization”, and “tag” variants.
Prevents user prompts like “tag this” or “categorise as” from falling into “other”.
Minimal change:
If you adopt the shared helper suggested in constants.ts, this becomes:
Quick check for other spots needing the same update:
🏁 Script executed:
Length of output: 158
🏁 Script executed:
Length of output: 14224
🏁 Script executed:
Length of output: 53
Cover all label-like variants and DRY the logic
• ExamplesList.tsx (lines 74–76)
• constants.ts (lines 84–86)
with:
🤖 Prompt for AI Agents