diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx index 254f31662a..952cbb91d7 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/ActionSummaryCard.tsx @@ -11,14 +11,18 @@ import { AWAITING_REPLY_LABEL_NAME, NEEDS_REPLY_LABEL_NAME, } from "@/utils/reply-tracker/consts"; +import { getEmailTerminology } from "@/utils/terminology"; export function ActionSummaryCard({ action, typeOptions, + provider, }: { action: CreateRuleBody["actions"][number]; typeOptions: { label: string; value: ActionType }[]; + provider: string; }) { + const terminology = getEmailTerminology(provider); const actionTypeLabel = typeOptions.find((opt) => opt.value === action.type)?.label || action.type; @@ -29,9 +33,11 @@ export function ActionSummaryCard({ case ActionType.LABEL: { const labelValue = action.label?.value || ""; if (action.label?.ai) { - summaryContent = labelValue ? `AI Label: ${labelValue}` : "AI Label"; + summaryContent = labelValue + ? `AI ${terminology.label.action}: ${labelValue}` + : `AI ${terminology.label.action}`; } else { - summaryContent = `Label as "${labelValue || "unset"}"`; + summaryContent = `${terminology.label.action} as "${labelValue || "unset"}"`; } break; } @@ -170,8 +176,8 @@ export function ActionSummaryCard({ break; case ActionType.TRACK_THREAD: - summaryContent = "Auto-update reply label"; - tooltipText = `Our AI will automatically update the thread label to '${NEEDS_REPLY_LABEL_NAME}' or '${AWAITING_REPLY_LABEL_NAME}' based on whether you need to respond or are awaiting a response from the recipient.`; + summaryContent = `Auto-update reply ${terminology.label.singular}`; + tooltipText = `Our AI will automatically update the thread ${terminology.label.singular} to '${NEEDS_REPLY_LABEL_NAME}' or '${AWAITING_REPLY_LABEL_NAME}' based on whether you need to respond or are awaiting a response from the recipient.`; break; case ActionType.ARCHIVE: diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/ExecutedRulesTable.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/ExecutedRulesTable.tsx index 2a408dc07d..89b8233f1b 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/ExecutedRulesTable.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/ExecutedRulesTable.tsx @@ -148,13 +148,15 @@ export function RuleCell({ export function ActionItemsCell({ actionItems, + provider, }: { actionItems: PendingExecutedRules["executedRules"][number]["actionItems"]; + provider: string; }) { return (
{actionItems.map((item) => ( - + ))}
); diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/Pending.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/Pending.tsx index a00de97f2b..e7fa93f209 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/Pending.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/Pending.tsx @@ -69,7 +69,7 @@ function PendingTable({ totalPages: number; mutate: () => void; }) { - const { emailAccountId, userEmail } = useAccount(); + const { emailAccountId, userEmail, provider } = useAccount(); const { selected, isAllSelected, onToggleSelect, onToggleSelectAll } = useToggleSelect(pending); @@ -191,7 +191,10 @@ function PendingTable({ /> - + diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/PersonaDialog.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/PersonaDialog.tsx index 605be2ead8..c043c591a5 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/PersonaDialog.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/PersonaDialog.tsx @@ -2,16 +2,18 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { ButtonList } from "@/components/ButtonList"; -import { personas } from "@/app/(app)/[emailAccountId]/assistant/examples"; +import type { Personas } from "./examples"; export function PersonaDialog({ isOpen, setIsOpen, onSelect, + personas, }: { isOpen: boolean; setIsOpen: (open: boolean) => void; onSelect: (persona: string) => void; + personas: Personas; }) { return ( diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx index 24697600e9..0aaca2853c 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx @@ -69,6 +69,7 @@ import { useAccount } from "@/providers/EmailAccountProvider"; import { prefixPath } from "@/utils/path"; import { useRule } from "@/hooks/useRule"; import { isMicrosoftProvider } from "@/utils/email/provider-types"; +import { getEmailTerminology } from "@/utils/terminology"; import { Dialog, DialogContent, @@ -324,11 +325,12 @@ export function RuleForm({ }, [errors, watch]); const conditionalOperator = watch("conditionalOperator"); + const terminology = getEmailTerminology(provider); const typeOptions = useMemo(() => { const options: { label: string; value: ActionType }[] = [ { label: "Archive", value: ActionType.ARCHIVE }, - { label: "Label", value: ActionType.LABEL }, + { label: terminology.label.action, value: ActionType.LABEL }, ...(isMicrosoftProvider(provider) ? [{ label: "Move to folder", value: ActionType.MOVE_FOLDER }] : []), @@ -340,11 +342,14 @@ export function RuleForm({ { label: "Mark spam", value: ActionType.MARK_SPAM }, { label: "Digest", value: ActionType.DIGEST }, { label: "Call webhook", value: ActionType.CALL_WEBHOOK }, - { label: "Auto-update reply label", value: ActionType.TRACK_THREAD }, + { + label: `Auto-update reply ${terminology.label.singular}`, + value: ActionType.TRACK_THREAD, + }, ]; return options; - }, [provider]); + }, [provider, terminology.label.action, terminology.label.singular]); const [isNameEditMode, setIsNameEditMode] = useState(alwaysEditMode); const [isConditionsEditMode, setIsConditionsEditMode] = @@ -849,6 +854,7 @@ export function RuleForm({ key={i} action={action} typeOptions={typeOptions} + provider={provider} /> ), )} @@ -870,7 +876,7 @@ export function RuleForm({ )} -
+
ruleDialog.open(); - const { emailAccountId } = useAccount(); + const { emailAccountId, provider } = useAccount(); const { createAssistantUrl } = useAssistantNavigation(emailAccountId); const { executeAsync: setRuleEnabled } = useAction( setRuleEnabledAction.bind(null, emailAccountId), @@ -275,7 +275,10 @@ export function Rules({ size = "md" }: { size?: "sm" | "md" }) { )} - + {/* {size === "md" && ( @@ -465,6 +468,7 @@ export function Rules({ size = "md" }: { size?: "sm" | "md" }) { export function ActionBadges({ actions, + provider, }: { actions: { id: string; @@ -472,6 +476,7 @@ export function ActionBadges({ label?: string | null; folderName?: string | null; }[]; + provider: string; }) { return (
@@ -485,7 +490,7 @@ export function ActionBadges({ color={getActionColor(action.type)} className="w-fit text-nowrap" > - {getActionDisplay(action)} + {getActionDisplay(action, provider)} ); })} diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx index d9ccd58e76..05142ab827 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx @@ -21,8 +21,9 @@ import { LoadingContent } from "@/components/LoadingContent"; import { Tooltip } from "@/components/Tooltip"; import { AssistantOnboarding } from "@/app/(app)/[emailAccountId]/assistant/AssistantOnboarding"; import { - examplePrompts, - personas, + getExamplePrompts, + getPersonas, + type Personas, } from "@/app/(app)/[emailAccountId]/assistant/examples"; import { convertLabelsToDisplay } from "@/utils/mention"; import { PersonaDialog } from "@/app/(app)/[emailAccountId]/assistant/PersonaDialog"; @@ -39,7 +40,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { useLabels } from "@/hooks/useLabels"; export function RulesPrompt() { - const { emailAccountId } = useAccount(); + const { emailAccountId, provider } = useAccount(); const { data, isLoading, error, mutate } = useSWR< RulesPromptResponse, { error: string } @@ -51,6 +52,8 @@ export function RulesPrompt() { ); const [persona, setPersona] = useState(null); + const personas = getPersonas(provider); + const examplePrompts = getExamplePrompts(provider); const personaPrompt = persona ? personas[persona as keyof typeof personas]?.prompt @@ -72,6 +75,8 @@ export function RulesPrompt() { mutate={mutate} onOpenPersonaDialog={onOpenPersonaDialog} showExamples + personas={personas} + examplePrompts={examplePrompts} /> { @@ -85,6 +90,7 @@ export function RulesPrompt() { isOpen={isModalOpen} setIsOpen={setIsModalOpen} onSelect={setPersona} + personas={personas} /> ); @@ -97,6 +103,8 @@ function RulesPromptForm({ mutate, onOpenPersonaDialog, showExamples, + personas, + examplePrompts, }: { emailAccountId: string; rulesPrompt: string | null; @@ -104,6 +112,8 @@ function RulesPromptForm({ mutate: () => void; onOpenPersonaDialog: () => void; showExamples?: boolean; + personas: Personas; + examplePrompts: string[]; }) { const { userLabels, isLoading: isLoadingLabels } = useLabels(); @@ -345,13 +355,24 @@ function RulesPromptForm({
- {showExamples && } + {showExamples && ( + + )}
); } -function PureExamples({ onSelect }: { onSelect: (example: string) => void }) { +function PureExamples({ + onSelect, + examplePrompts, +}: { + onSelect: (example: string) => void; + examplePrompts: string[]; +}) { return (
Examples diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts b/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts index 473bc4945b..8c73b0c248 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts +++ b/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts @@ -1,3 +1,7 @@ +import { getEmailTerminology } from "@/utils/terminology"; + +export type Personas = ReturnType; + // NOTE: some users save the example rules when trying out the platform, and start auto sending emails // to people without realising it. This is a simple check to avoid that. // This needs changing when the examples change. But it works for now. @@ -21,12 +25,29 @@ function formatPromptArray(promptArray: string[]): string { return `${promptArray.map((item) => `* ${item}`).join(".\n")}.`; } +function processPromptsWithTerminology( + prompts: string[], + provider: string, +): string[] { + const terminology = getEmailTerminology(provider); + return prompts.map((prompt) => { + // Replace "Label" at the beginning of sentences or after punctuation + let processed = prompt.replace(/\bLabel\b/g, terminology.label.action); + // Replace lowercase "label" in the middle of sentences + processed = processed.replace( + /\blabel\b/g, + terminology.label.action.toLowerCase(), + ); + return processed; + }); +} + const commonPrompts = [ "Label urgent emails as @[Urgent]", "Label emails from @mycompany.com addresses as @[Team]", ]; -export const examplePrompts = [ +const examplePromptsBase = [ ...commonPrompts, "Forward receipts to jane@accounting.com and label them @[Receipt]", "Forward pitch decks to john@investing.com and label them @[Pitch Deck]", @@ -46,6 +67,10 @@ export const examplePrompts = [ "Label Stripe emails as @[Stripe]", ]; +export function getExamplePrompts(provider: string): string[] { + return processPromptsWithTerminology(examplePromptsBase, provider); +} + const founderPromptArray = [ ...commonPrompts, "If someone asks to set up a call, draft a reply with my calendar link: https://cal.com/example", @@ -57,232 +82,267 @@ const founderPromptArray = [ "Label recruitment related emails as @[Hiring]", ]; -export const personas = { - founder: { - label: "🚀 Founder", - promptArray: founderPromptArray, - get prompt() { - return formatPromptArray(this.promptArray); +export function getPersonas(provider: string) { + return { + founder: { + label: "🚀 Founder", + promptArray: processPromptsWithTerminology(founderPromptArray, provider), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - influencer: { - label: "📹 Influencer", - promptArray: [ - ...commonPrompts, - `Label sponsorship inquiries as @[Sponsorship] and draft a reply as follows: + influencer: { + label: "📹 Influencer", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + `Label sponsorship inquiries as @[Sponsorship] and draft a reply as follows: > Hey NAME, > > I've attached my media kit and pricing`, - "Label emails about affiliate programs as @[Affiliate] and archive them", - "Label collaboration requests as @[Collab] and draft a reply asking about their audience size and engagement rates", - "Label brand partnership emails as @[Brand Deal] and forward to manager@example.com", - "Label media inquiries as @[Press] and draft a reply a polite reply", - ], - get prompt() { - return formatPromptArray(this.promptArray); + "Label emails about affiliate programs as @[Affiliate] and archive them", + "Label collaboration requests as @[Collab] and draft a reply asking about their audience size and engagement rates", + "Label brand partnership emails as @[Brand Deal] and forward to manager@example.com", + "Label media inquiries as @[Press] and draft a reply a polite reply", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - realtor: { - label: "🏠 Realtor", - promptArray: [ - ...commonPrompts, - "Label emails from potential buyers as @[Buyer Lead] and draft a reply asking about their budget and preferred neighborhoods", - "Label emails from potential sellers as @[Seller Lead] and draft a reply with my calendar link to schedule a home evaluation: https://cal.com/example", - "If someone asks about home prices in a specific area, label as @[Market Inquiry] and draft a reply with recent comparable sales data", - "Label emails from mortgage brokers and lenders as @[Lender] and archive them", - "If someone asks to schedule a showing, label as @[Showing Request] and draft a reply with available time slots", - "Label emails about closing documents as @[Closing] and forward to transactions@realty.com", - "If someone asks about the home buying process, draft a reply with our buyer's guide link: https://realty.com/buyers-guide", - "Label emails from home inspectors as @[Inspector] and forward to scheduling@realty.com", - "If someone refers a client to me, label as @[Referral] and draft a thank you reply with my calendar link to schedule a consultation", - ], - get prompt() { - return formatPromptArray(this.promptArray); + realtor: { + label: "🏠 Realtor", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label emails from potential buyers as @[Buyer Lead] and draft a reply asking about their budget and preferred neighborhoods", + "Label emails from potential sellers as @[Seller Lead] and draft a reply with my calendar link to schedule a home evaluation: https://cal.com/example", + "If someone asks about home prices in a specific area, label as @[Market Inquiry] and draft a reply with recent comparable sales data", + "Label emails from mortgage brokers and lenders as @[Lender] and archive them", + "If someone asks to schedule a showing, label as @[Showing Request] and draft a reply with available time slots", + "Label emails about closing documents as @[Closing] and forward to transactions@realty.com", + "If someone asks about the home buying process, draft a reply with our buyer's guide link: https://realty.com/buyers-guide", + "Label emails from home inspectors as @[Inspector] and forward to scheduling@realty.com", + "If someone refers a client to me, label as @[Referral] and draft a thank you reply with my calendar link to schedule a consultation", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - investor: { - label: "💰 Investor", - promptArray: [ - ...commonPrompts, - "If a founder asks to set up a call, draft a reply with my calendar link: https://cal.com/example", - "If a founder sends me an investor update, label it @[Investor Update] and archive it", - "Forward pitch decks to analyst@vc.com that asks them to review it and label them @[Pitch Deck]", - "Label emails from LPs as @[LP]", - "Label legal documents as @[Legal]", - "Label emails about travel as @[Travel]", - "Label emails about portfolio company exits as @[Exit Opportunity]", - "Label emails containing term sheets as @[Term Sheet]", - "If a portfolio company reports bad news, label as @[Portfolio Alert] and draft a reply to schedule an emergency call", - "Label due diligence related emails as @[Due Diligence]", - "Forward emails about industry research reports to research@vc.com", - "If someone asks for a warm intro to a portfolio company, draft a reply asking for more context about why they want to connect", - "Label emails about fund administration as @[Fund Admin]", - "Label emails about speaking at investment conferences as @[Speaking Opportunity]", - ], - get prompt() { - return formatPromptArray(this.promptArray); + investor: { + label: "💰 Investor", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "If a founder asks to set up a call, draft a reply with my calendar link: https://cal.com/example", + "If a founder sends me an investor update, label it @[Investor Update] and archive it", + "Forward pitch decks to analyst@vc.com that asks them to review it and label them @[Pitch Deck]", + "Label emails from LPs as @[LP]", + "Label legal documents as @[Legal]", + "Label emails about travel as @[Travel]", + "Label emails about portfolio company exits as @[Exit Opportunity]", + "Label emails containing term sheets as @[Term Sheet]", + "If a portfolio company reports bad news, label as @[Portfolio Alert] and draft a reply to schedule an emergency call", + "Label due diligence related emails as @[Due Diligence]", + "Forward emails about industry research reports to research@vc.com", + "If someone asks for a warm intro to a portfolio company, draft a reply asking for more context about why they want to connect", + "Label emails about fund administration as @[Fund Admin]", + "Label emails about speaking at investment conferences as @[Speaking Opportunity]", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - assistant: { - label: "📋 Assistant", - promptArray: founderPromptArray, - get prompt() { - return formatPromptArray(this.promptArray); + assistant: { + label: "📋 Assistant", + promptArray: processPromptsWithTerminology(founderPromptArray, provider), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - developer: { - label: "👨‍💻 Developer", - promptArray: [ - ...commonPrompts, - "Label server errors, deployment failures, and other server alerts as @[Alert] and forward to oncall@company.com", - "Label emails from GitHub as @[GitHub] and archive them", - "Label emails from Figma as @[Design] and archive them", - "Label emails from Stripe as @[Stripe] and archive them", - "Label emails from Slack as @[Slack] and archive them", - "Label emails about bug reports as @[Bug]", - "If someone reports a security vulnerability, label as @[Security] and forward to security@company.com", - "Label emails about job interviews as @[Job Search]", - "Label emails from recruiters as @[Recruiter] and archive them", - ], - get prompt() { - return formatPromptArray(this.promptArray); + developer: { + label: "👨‍💻 Developer", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label server errors, deployment failures, and other server alerts as @[Alert] and forward to oncall@company.com", + "Label emails from GitHub as @[GitHub] and archive them", + "Label emails from Figma as @[Design] and archive them", + "Label emails from Stripe as @[Stripe] and archive them", + "Label emails from Slack as @[Slack] and archive them", + "Label emails about bug reports as @[Bug]", + "If someone reports a security vulnerability, label as @[Security] and forward to security@company.com", + "Label emails about job interviews as @[Job Search]", + "Label emails from recruiters as @[Recruiter] and archive them", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - designer: { - label: "🎨 Designer", - promptArray: [ - ...commonPrompts, - "Label emails from Figma, Adobe, Sketch, and other design tools as @[Design] and archive them", - "Label emails from clients as @[Client]", - "If someone sends design assets, label as @[Design Assets] and forward to assets@company.com", - "Label emails from Dribbble, Behance, and other design inspiration sites as @[Inspiration] and archive them", - "Label emails about design conferences as @[Conference]", - "If someone requests brand assets, draft a reply with a link to our brand portal: https://brand.company.com", - "Label emails about user research as @[Research]", - "Label emails about job interviews as @[Job Search]", - "Label emails from recruiters as @[Recruiter] and archive them", - ], - get prompt() { - return formatPromptArray(this.promptArray); + designer: { + label: "🎨 Designer", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label emails from Figma, Adobe, Sketch, and other design tools as @[Design] and archive them", + "Label emails from clients as @[Client]", + "If someone sends design assets, label as @[Design Assets] and forward to assets@company.com", + "Label emails from Dribbble, Behance, and other design inspiration sites as @[Inspiration] and archive them", + "Label emails about design conferences as @[Conference]", + "If someone requests brand assets, draft a reply with a link to our brand portal: https://brand.company.com", + "Label emails about user research as @[Research]", + "Label emails about job interviews as @[Job Search]", + "Label emails from recruiters as @[Recruiter] and archive them", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - sales: { - label: "🤝 Sales", - promptArray: [ - ...commonPrompts, - "Label emails from prospects as @[Prospect]", - "Label emails from customers as @[Customer]", - "Label emails about deal negotiations as @[Deal Discussion]", - "Label emails from sales tools as @[Sales Tool] and archive them", - "Label emails about sales opportunities as @[Sales Opportunity]", - "If someone asks for pricing, draft a reply with our pricing page link: https://company.com/pricing", - "Label emails containing signed contracts as @[Signed Contract] and forward to legal@company.com", - "If someone requests a demo, draft a reply with my calendar link: https://cal.com/example", - "If someone asks about product features, draft a reply with relevant feature documentation links", - "If someone reports implementation issues, label as @[Support Need] and forward to support@company.com", - "If someone asks about enterprise pricing, draft a reply asking about their company size and requirements", - "If a customer mentions churn risk, label as @[Churn Risk] and draft an urgent notification to the customer success team", - ], - get prompt() { - return formatPromptArray(this.promptArray); + sales: { + label: "🤝 Sales", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label emails from prospects as @[Prospect]", + "Label emails from customers as @[Customer]", + "Label emails about deal negotiations as @[Deal Discussion]", + "Label emails from sales tools as @[Sales Tool] and archive them", + "Label emails about sales opportunities as @[Sales Opportunity]", + "If someone asks for pricing, draft a reply with our pricing page link: https://company.com/pricing", + "Label emails containing signed contracts as @[Signed Contract] and forward to legal@company.com", + "If someone requests a demo, draft a reply with my calendar link: https://cal.com/example", + "If someone asks about product features, draft a reply with relevant feature documentation links", + "If someone reports implementation issues, label as @[Support Need] and forward to support@company.com", + "If someone asks about enterprise pricing, draft a reply asking about their company size and requirements", + "If a customer mentions churn risk, label as @[Churn Risk] and draft an urgent notification to the customer success team", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - marketer: { - label: "📢 Marketer", - promptArray: [ - ...commonPrompts, - "Label emails from influencers as @[Influencer]", - "Label emails from ad platforms (Google, Meta, LinkedIn) as @[Advertising]", - "Label press inquiries as @[Press] and forward to pr@company.com", - "Label emails about content marketing as @[Content]", - "If someone asks about sponsorship, label as @[Sponsorship] and draft a reply asking about their audience size", - "If someone requests to guest post, label as @[Guest Post] and draft a reply with our guidelines", - "If someone asks about partnership opportunities, label as @[Partnership] and draft a reply asking for their media kit", - "If someone reports broken marketing links, label as @[Bug] and forward to tech@company.com", - ], - get prompt() { - return formatPromptArray(this.promptArray); + marketer: { + label: "📢 Marketer", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label emails from influencers as @[Influencer]", + "Label emails from ad platforms (Google, Meta, LinkedIn) as @[Advertising]", + "Label press inquiries as @[Press] and forward to pr@company.com", + "Label emails about content marketing as @[Content]", + "If someone asks about sponsorship, label as @[Sponsorship] and draft a reply asking about their audience size", + "If someone requests to guest post, label as @[Guest Post] and draft a reply with our guidelines", + "If someone asks about partnership opportunities, label as @[Partnership] and draft a reply asking for their media kit", + "If someone reports broken marketing links, label as @[Bug] and forward to tech@company.com", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - support: { - label: "🛠️ Support", - promptArray: [ - ...commonPrompts, - "Label customer support tickets as @[Support Ticket]", - "If someone reports a critical issue, label as @[Urgent Support] and forward to urgent@company.com", - "Label bug reports as @[Bug] and forward to engineering@company.com", - "Label feature requests as @[Feature Request] and forward to product@company.com", - "If someone asks for refund, draft a reply with our refund policy link: https://company.com/refund-policy", - "Label emails about account access issues as @[Access Issue] and draft a reply asking for their account details", - "If someone asks for product documentation, draft a reply with our help center link: https://help.company.com", - "Label emails about service outages as @[Service Issue] and forward to status@company.com", - "If someone needs technical assistance, draft a reply asking for their account details and specific error messages", - "Label positive feedback as @[Testimonial] and forward to marketing@company.com", - "Label emails about API integration issues as @[API Support]", - "If someone reports data privacy concerns, label as @[Privacy], and draft a reply with our privacy policy link: https://company.com/privacy-policy", - ], - get prompt() { - return formatPromptArray(this.promptArray); + support: { + label: "🛠️ Support", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label customer support tickets as @[Support Ticket]", + "If someone reports a critical issue, label as @[Urgent Support] and forward to urgent@company.com", + "Label bug reports as @[Bug] and forward to engineering@company.com", + "Label feature requests as @[Feature Request] and forward to product@company.com", + "If someone asks for refund, draft a reply with our refund policy link: https://company.com/refund-policy", + "Label emails about account access issues as @[Access Issue] and draft a reply asking for their account details", + "If someone asks for product documentation, draft a reply with our help center link: https://help.company.com", + "Label emails about service outages as @[Service Issue] and forward to status@company.com", + "If someone needs technical assistance, draft a reply asking for their account details and specific error messages", + "Label positive feedback as @[Testimonial] and forward to marketing@company.com", + "Label emails about API integration issues as @[API Support]", + "If someone reports data privacy concerns, label as @[Privacy], and draft a reply with our privacy policy link: https://company.com/privacy-policy", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - recruiter: { - label: "👥 Recruiter", - promptArray: [ - ...commonPrompts, - "Label emails from candidates as @[Candidate]", - "Label emails from hiring managers as @[Hiring Manager]", - "Label emails from recruiters as @[Recruiter] and draft a reply with our hiring process overview link: https://company.com/hiring-process", - "Label emails from job boards as @[Job Board] and archive them", - "Label emails from LinkedIn as @[LinkedIn] and archive them", - "If someone applies for a job, label as @[New Application] and draft a reply acknowledging their application", - "Label emails containing resumes or CVs as @[Resume]", - "If a candidate asks about application status, label as @[Status Update] and draft a reply asking for their position and date applied", - "Label emails about interview scheduling as @[Interview Scheduling]", - "If someone accepts an interview invite, label as @[Interview Confirmed] and forward to calendar@company.com", - "If someone declines a job offer, label as @[Offer Declined] and forward to hiring-updates@company.com", - "If someone accepts a job offer, label as @[Offer Accepted] and forward to onboarding@company.com", - "Label emails about salary negotiations as @[Compensation]", - "Label emails about reference checks as @[References]", - "If someone asks about benefits, draft a reply with our benefits overview link: https://company.com/benefits", - "Label emails about background checks as @[Background Check]", - "If an internal employee refers someone, label as @[Employee Referral]", - "Label emails about recruitment events or job fairs as @[Recruiting Event]", - "If someone withdraws their application, label as @[Withdrawn]", - ], - get prompt() { - return formatPromptArray(this.promptArray); + recruiter: { + label: "👥 Recruiter", + promptArray: processPromptsWithTerminology( + [ + ...commonPrompts, + "Label emails from candidates as @[Candidate]", + "Label emails from hiring managers as @[Hiring Manager]", + "Label emails from recruiters as @[Recruiter] and draft a reply with our hiring process overview link: https://company.com/hiring-process", + "Label emails from job boards as @[Job Board] and archive them", + "Label emails from LinkedIn as @[LinkedIn] and archive them", + "If someone applies for a job, label as @[New Application] and draft a reply acknowledging their application", + "Label emails containing resumes or CVs as @[Resume]", + "If a candidate asks about application status, label as @[Status Update] and draft a reply asking for their position and date applied", + "Label emails about interview scheduling as @[Interview Scheduling]", + "If someone accepts an interview invite, label as @[Interview Confirmed] and forward to calendar@company.com", + "If someone declines a job offer, label as @[Offer Declined] and forward to hiring-updates@company.com", + "If someone accepts a job offer, label as @[Offer Accepted] and forward to onboarding@company.com", + "Label emails about salary negotiations as @[Compensation]", + "Label emails about reference checks as @[References]", + "If someone asks about benefits, draft a reply with our benefits overview link: https://company.com/benefits", + "Label emails about background checks as @[Background Check]", + "If an internal employee refers someone, label as @[Employee Referral]", + "Label emails about recruitment events or job fairs as @[Recruiting Event]", + "If someone withdraws their application, label as @[Withdrawn]", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - student: { - label: "👩‍🎓 Student", - promptArray: [ - "Label emails from professors and teaching assistants as @[School]", - "Label emails about assignments and homework as @[Assignment]", - "If someone sends class notes or study materials, label as @[Study Materials]", - "Label emails about internships as @[Internship] and forward to my personal email me@example.com", - "Label emails about exam schedules as @[Exam]", - "Label emails about campus events as @[Event] and archive them", - "If someone asks for class notes, draft a reply with our shared Google Drive folder link: https://drive.google.com/drive/u/0/folders/1234567890", - "Label emails about tutoring opportunities as @[Tutoring] and draft a reply with that my rate is $70/hour or $40/hour for group tutoring", - ], - get prompt() { - return formatPromptArray(this.promptArray); + student: { + label: "👩‍🎓 Student", + promptArray: processPromptsWithTerminology( + [ + "Label emails from professors and teaching assistants as @[School]", + "Label emails about assignments and homework as @[Assignment]", + "If someone sends class notes or study materials, label as @[Study Materials]", + "Label emails about internships as @[Internship] and forward to my personal email me@example.com", + "Label emails about exam schedules as @[Exam]", + "Label emails about campus events as @[Event] and archive them", + "If someone asks for class notes, draft a reply with our shared Google Drive folder link: https://drive.google.com/drive/u/0/folders/1234567890", + "Label emails about tutoring opportunities as @[Tutoring] and draft a reply with that my rate is $70/hour or $40/hour for group tutoring", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - reachout: { - label: "💬 Outreach", - promptArray: [ - "If someone replies to me that they're interested, label it @[Interested] and draft a reply with my calendar link: https://cal.com/example", - ], - get prompt() { - return formatPromptArray(this.promptArray); + reachout: { + label: "💬 Outreach", + promptArray: processPromptsWithTerminology( + [ + "If someone replies to me that they're interested, label it @[Interested] and draft a reply with my calendar link: https://cal.com/example", + ], + provider, + ), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, - other: { - label: "🤖 Other", - promptArray: examplePrompts, - get prompt() { - return formatPromptArray(this.promptArray); + other: { + label: "🤖 Other", + promptArray: getExamplePrompts(provider), + get prompt() { + return formatPromptArray(this.promptArray); + }, }, - }, -}; + }; +} diff --git a/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/common.tsx b/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/common.tsx index f2e74c8fa9..cd33258914 100644 --- a/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/common.tsx +++ b/apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/common.tsx @@ -53,6 +53,7 @@ import { LabelsSubMenu } from "@/components/LabelsSubMenu"; import type { EmailLabel } from "@/providers/EmailProvider"; import { useAccount } from "@/providers/EmailAccountProvider"; import { isGoogleProvider } from "@/utils/email/provider-types"; +import { getEmailTerminology } from "@/utils/terminology"; export function ActionCell({ item, @@ -226,6 +227,8 @@ function AutoArchiveButton({ labels: EmailLabel[]; emailAccountId: string; }) { + const { provider } = useAccount(); + const terminology = getEmailTerminology(provider); const { autoArchiveLoading, onAutoArchive, @@ -306,7 +309,9 @@ function AutoArchiveButton({ )} - Skip Inbox and Label + + Skip Inbox and {terminology.label.singularCapitalized} + {labels.map((label) => { return ( @@ -323,8 +328,9 @@ function AutoArchiveButton({ })} {!labels.length && ( - You do not have any labels. Create one in Gmail first to auto - label emails. + You do not have any {terminology.label.plural}. Create one in your + email client first to auto + {terminology.label.singular} emails. )} @@ -389,6 +395,7 @@ export function MoreDropdown({ posthog: PostHog; }) { const { provider } = useAccount(); + const terminology = getEmailTerminology(provider); const { archiveAllLoading, onArchiveAll } = useArchiveAll({ item, posthog, @@ -440,7 +447,7 @@ export function MoreDropdown({ - Label future emails + {terminology.label.singularCapitalized} future emails ; @@ -28,6 +29,15 @@ export default async function RuleHistoryPage(props: { userId: session.user.id, }, }, + select: { + id: true, + name: true, + emailAccount: { + select: { + account: { select: { provider: true } }, + }, + }, + }, }); if (!rule) notFound(); @@ -183,7 +193,14 @@ export default async function RuleHistoryPage(props: { {action.type} {action.label && ( - Label: {action.label} + + { + getEmailTerminology( + rule.emailAccount.account.provider, + ).label.action + } + : {action.label} + )} {action.subject && ( Subject: {action.subject} diff --git a/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx b/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx index a6eb397e6b..c4932b966f 100644 --- a/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx +++ b/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx @@ -16,6 +16,8 @@ import { prefixPath } from "@/utils/path"; import { useSetupProgress } from "@/hooks/useSetupProgress"; import { LoadingContent } from "@/components/LoadingContent"; import { EXTENSION_URL } from "@/utils/config"; +import { isGoogleProvider } from "@/utils/email/provider-types"; +import { useAccount } from "@/providers/EmailAccountProvider"; function FeatureCard({ emailAccountId, @@ -163,6 +165,7 @@ const StepItem = ({ function Checklist({ emailAccountId, + provider, completedCount, totalSteps, progressPercentage, @@ -171,6 +174,7 @@ function Checklist({ isAiAssistantConfigured, }: { emailAccountId: string; + provider: string; completedCount: number; totalSteps: number; progressPercentage: number; @@ -230,22 +234,25 @@ function Checklist({ actionText="View" /> - } - iconBg="bg-orange-100 dark:bg-orange-900/50" - iconColor="text-orange-500 dark:text-orange-400" - title="Install the Inbox Zero Tabs extension" - timeEstimate="1 minute" - completed={false} - actionText="Install" - /> + {isGoogleProvider(provider) && ( + } + iconBg="bg-orange-100 dark:bg-orange-900/50" + iconColor="text-orange-500 dark:text-orange-400" + title="Install the Inbox Zero Tabs extension" + timeEstimate="1 minute" + completed={false} + actionText="Install" + /> + )} ); } -export function SetupContent({ emailAccountId }: { emailAccountId: string }) { +export function SetupContent() { + const { emailAccountId, provider } = useAccount(); const { data, isLoading, error } = useSetupProgress(); return ( @@ -253,6 +260,7 @@ export function SetupContent({ emailAccountId }: { emailAccountId: string }) { {data && ( - + ); diff --git a/apps/web/app/(app)/accounts/AddAccount.tsx b/apps/web/app/(app)/accounts/AddAccount.tsx index 30a1ca39d2..72fa9444e0 100644 --- a/apps/web/app/(app)/accounts/AddAccount.tsx +++ b/apps/web/app/(app)/accounts/AddAccount.tsx @@ -130,7 +130,7 @@ function AddEmailAccount({ - + Add {name} Account @@ -146,7 +146,7 @@ function AddEmailAccount({ disabled={isMerging || isConnecting} onClick={onMerge} > - Yes + Yes, it's an existing Inbox Zero account diff --git a/apps/web/components/LabelsSubMenu.tsx b/apps/web/components/LabelsSubMenu.tsx index e73bfaa27b..b98d7fc172 100644 --- a/apps/web/components/LabelsSubMenu.tsx +++ b/apps/web/components/LabelsSubMenu.tsx @@ -3,6 +3,8 @@ import { DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import type { EmailLabel } from "@/providers/EmailProvider"; +import { useAccount } from "@/providers/EmailAccountProvider"; +import { getEmailTerminology } from "@/utils/terminology"; export function LabelsSubMenu({ labels, @@ -11,6 +13,9 @@ export function LabelsSubMenu({ labels: EmailLabel[]; onClick: (label: EmailLabel) => void; }) { + const { provider } = useAccount(); + const terminology = getEmailTerminology(provider); + return ( {labels.length ? ( @@ -22,7 +27,9 @@ export function LabelsSubMenu({ ); }) ) : ( - You don't have any labels yet. + + You don't have any {terminology.label.plural} yet. + )} ); diff --git a/apps/web/components/PlanBadge.tsx b/apps/web/components/PlanBadge.tsx index 1d853d7291..d10bd9a5c6 100644 --- a/apps/web/components/PlanBadge.tsx +++ b/apps/web/components/PlanBadge.tsx @@ -9,14 +9,15 @@ import { type Rule, } from "@prisma/client"; import { truncate } from "@/utils/string"; +import { getEmailTerminology } from "@/utils/terminology"; type Plan = Pick & { rule: Rule | null; actionItems: ExecutedAction[]; }; -export function PlanBadge(props: { plan?: Plan }) { - const { plan } = props; +export function PlanBadge(props: { plan?: Plan; provider: string }) { + const { plan, provider } = props; // if (!plan) return Not planned; if (!plan) return null; @@ -59,7 +60,7 @@ export function PlanBadge(props: { plan?: Plan }) { color={getActionColor(action.type)} className="whitespace-pre-wrap" > - {getActionMessage(action)} + {getActionMessage(action, provider)}
); @@ -78,16 +79,34 @@ export function PlanBadge(props: { plan?: Plan }) { ); } -export function ActionBadge({ type }: { type: ActionType }) { - return {getActionLabel(type)}; +export function ActionBadge({ + type, + provider, +}: { + type: ActionType; + provider: string; +}) { + return ( + {getActionLabel(type, provider)} + ); } -export function ActionBadgeExpanded({ action }: { action: ExecutedAction }) { +export function ActionBadgeExpanded({ + action, + provider, +}: { + action: ExecutedAction; + provider: string; +}) { switch (action.type) { case ActionType.ARCHIVE: - return ; + return ; case ActionType.LABEL: - return Label: "{action.label}"; + return ( + + {getEmailTerminology(provider).label.action}: "{action.label}" + + ); case ActionType.REPLY: return (
@@ -117,13 +136,13 @@ export function ActionBadgeExpanded({ action }: { action: ExecutedAction }) {
); case ActionType.MARK_SPAM: - return ; + return ; case ActionType.CALL_WEBHOOK: - return ; + return ; case ActionType.MARK_READ: - return ; + return ; default: - return ; + return ; } } @@ -135,10 +154,12 @@ function ActionContent({ action }: { action: ExecutedAction }) { ); } -function getActionLabel(type: ActionType) { +function getActionLabel(type: ActionType, provider: string) { + const terminology = getEmailTerminology(provider); + switch (type) { case ActionType.LABEL: - return "Label"; + return terminology.label.singularCapitalized; case ActionType.ARCHIVE: return "Archive"; case ActionType.FORWARD: @@ -160,21 +181,24 @@ function getActionLabel(type: ActionType) { } } -function getActionMessage(action: ExecutedAction): string { +function getActionMessage(action: ExecutedAction, provider: string): string { + const terminology = getEmailTerminology(provider); + switch (action.type) { // biome-ignore lint/suspicious/noFallthroughSwitchClause: ignore case ActionType.LABEL: - if (action.label) return `Label: "${action.label}"`; + if (action.label) + return `${terminology.label.singularCapitalized}: "${action.label}"`; case ActionType.REPLY: case ActionType.SEND_EMAIL: // biome-ignore lint/suspicious/noFallthroughSwitchClause: ignore case ActionType.FORWARD: if (action.to) - return `${getActionLabel(action.type)} to ${action.to}${ + return `${getActionLabel(action.type, provider)} to ${action.to}${ action.content ? `:\n${action.content}` : "" }`; default: - return getActionLabel(action.type); + return getActionLabel(action.type, provider); } } diff --git a/apps/web/components/SideNav.tsx b/apps/web/components/SideNav.tsx index 16f653e1c8..c0b67624ec 100644 --- a/apps/web/components/SideNav.tsx +++ b/apps/web/components/SideNav.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from "react"; import { usePathname } from "next/navigation"; import Link from "next/link"; +import { getEmailTerminology } from "@/utils/terminology"; import { AlertCircleIcon, ArchiveIcon, @@ -236,6 +237,8 @@ function MailNav({ path }: { path: string }) { const { onOpen } = useComposeModal(); const [showHiddenLabels, setShowHiddenLabels] = useState(false); const { visibleLabels, hiddenLabels, isLoading } = useSplitLabels(); + const { provider } = useAccount(); + const terminology = getEmailTerminology(provider); // Transform user labels into NavItems const labelNavItems = useMemo(() => { @@ -292,13 +295,15 @@ function MailNav({ path }: { path: string }) { - Labels + + {terminology.label.pluralCapitalized} + {visibleLabels.length > 0 ? ( ) : (
- No labels + No {terminology.label.plural}
)} diff --git a/apps/web/components/assistant-chat/examples-dialog.tsx b/apps/web/components/assistant-chat/examples-dialog.tsx index 31d5a838ea..461133089e 100644 --- a/apps/web/components/assistant-chat/examples-dialog.tsx +++ b/apps/web/components/assistant-chat/examples-dialog.tsx @@ -16,7 +16,7 @@ import { PlusIcon, CheckCircle2Icon, } from "lucide-react"; -import { personas } from "@/app/(app)/[emailAccountId]/assistant/examples"; +import { getPersonas } from "@/app/(app)/[emailAccountId]/assistant/examples"; import { convertLabelsToDisplay, convertMentionsToLabels, @@ -25,6 +25,7 @@ import { Tooltip } from "@/components/Tooltip"; import { ButtonList } from "@/components/ButtonList"; import { parseAsStringEnum, useQueryState } from "nuqs"; import { cn } from "@/utils"; +import { useAccount } from "@/providers/EmailAccountProvider"; interface ExamplesDialogProps { setInput: (input: string) => void; @@ -39,6 +40,8 @@ export function ExamplesDialog({ open, onOpenChange, }: ExamplesDialogProps) { + const { provider } = useAccount(); + const personas = getPersonas(provider); const [internalOpen, setInternalOpen] = useState(false); const [selectedExamples, setSelectedExamples] = useState([]); diff --git a/apps/web/components/assistant-chat/tools.tsx b/apps/web/components/assistant-chat/tools.tsx index 7d879677c7..7b24a4ff0e 100644 --- a/apps/web/components/assistant-chat/tools.tsx +++ b/apps/web/components/assistant-chat/tools.tsx @@ -19,6 +19,7 @@ import { useAccount } from "@/providers/EmailAccountProvider"; import { ExpandableText } from "@/components/ExpandableText"; import { RuleDialog } from "@/app/(app)/[emailAccountId]/assistant/RuleDialog"; import { useDialogState } from "@/hooks/useDialogState"; +import { getEmailTerminology } from "@/utils/terminology"; export function BasicToolInfo({ text }: { text: string }) { return ( @@ -200,6 +201,7 @@ export function UpdatedRuleActions({ originalActions?: UpdateRuleActionsTool["output"]["originalActions"]; updatedActions?: UpdateRuleActionsTool["output"]["updatedActions"]; }) { + const { provider } = useAccount(); const [showChanges, setShowChanges] = useState(false); // Check if actions have changed by comparing serialized versions @@ -216,7 +218,10 @@ export function UpdatedRuleActions({ return actions .map((action) => { const parts = [`Type: ${action.type}`]; - if (action.fields?.label) parts.push(`Label: ${action.fields.label}`); + if (action.fields?.label) + parts.push( + `${getEmailTerminology(provider).label.action}: ${action.fields.label}`, + ); if (action.fields?.content) parts.push(`Content: ${action.fields.content}`); if (action.fields?.to) parts.push(`To: ${action.fields.to}`); diff --git a/apps/web/components/email-list/EmailList.tsx b/apps/web/components/email-list/EmailList.tsx index af3467a274..47d175e2c9 100644 --- a/apps/web/components/email-list/EmailList.tsx +++ b/apps/web/components/email-list/EmailList.tsx @@ -178,7 +178,7 @@ export function EmailList({ isLoadingMore?: boolean; handleLoadMore?: () => void; }) { - const { emailAccountId, userEmail } = useAccount(); + const { emailAccountId, userEmail, provider } = useAccount(); // if right panel is open const [openThreadId, setOpenThreadId] = useQueryState("thread-id"); @@ -477,6 +477,7 @@ export function EmailList({ } }} userEmail={userEmail} + provider={provider} thread={thread} opened={openThreadId === thread.id} closePanel={closePanel} diff --git a/apps/web/components/email-list/EmailListItem.tsx b/apps/web/components/email-list/EmailListItem.tsx index b4fb98c918..73f1c364a8 100644 --- a/apps/web/components/email-list/EmailListItem.tsx +++ b/apps/web/components/email-list/EmailListItem.tsx @@ -26,6 +26,7 @@ export const EmailListItem = forwardRef( ( props: { userEmail: string; + provider: string; thread: Thread; opened: boolean; selected: boolean; @@ -44,7 +45,7 @@ export const EmailListItem = forwardRef( }, ref: ForwardedRef, ) => { - const { thread, splitView, onSelected } = props; + const { provider, thread, splitView, onSelected } = props; const lastMessage = thread.messages?.[thread.messages.length - 1]; @@ -177,7 +178,7 @@ export const EmailListItem = forwardRef( {thread.category?.category ? ( ) : null} - + Promise; refetch: () => void; }) { + const { provider } = useAccount(); const isPlanning = useIsInAiQueue(row.id); const lastMessage = row.messages?.[row.messages.length - 1]; @@ -74,6 +76,7 @@ export function EmailPanel({ {plan?.rule && ( Promise; rejectPlan: (thread: Thread) => Promise; }) { - const { thread } = props; + const { provider, thread } = props; if (!thread) return null; const { plan } = thread; if (!plan?.rule) return null; @@ -21,7 +22,7 @@ export function PlanExplanation(props: {
- +
{plan.rule?.instructions}
diff --git a/apps/web/utils/action-display.ts b/apps/web/utils/action-display.ts index 675a375c29..543fd7dca8 100644 --- a/apps/web/utils/action-display.ts +++ b/apps/web/utils/action-display.ts @@ -1,16 +1,23 @@ import { capitalCase } from "capital-case"; import { ActionType } from "@prisma/client"; +import { getEmailTerminology } from "@/utils/terminology"; -export function getActionDisplay(action: { - type: ActionType; - label?: string | null; - folderName?: string | null; -}): string { +export function getActionDisplay( + action: { + type: ActionType; + label?: string | null; + folderName?: string | null; + }, + provider: string, +): string { + const terminology = getEmailTerminology(provider); switch (action.type) { case ActionType.DRAFT_EMAIL: return "Draft Reply"; case ActionType.LABEL: - return action.label ? `Label: ${action.label}` : "Label"; + return action.label + ? `${terminology.label.action}: ${action.label}` + : terminology.label.action; case ActionType.ARCHIVE: return "Skip Inbox"; case ActionType.MARK_READ: @@ -22,7 +29,7 @@ export function getActionDisplay(action: { case ActionType.CALL_WEBHOOK: return "Call Webhook"; case ActionType.TRACK_THREAD: - return "Auto-update reply label"; + return `Auto-update reply ${terminology.label.singular}`; case ActionType.MOVE_FOLDER: return action.folderName ? `Folder: ${action.folderName}` diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts index 9af3705273..fbe5629275 100644 --- a/apps/web/utils/actions/rule.ts +++ b/apps/web/utils/actions/rule.ts @@ -56,7 +56,7 @@ export const createRuleAction = actionClient .schema(createRuleBody) .action( async ({ - ctx: { emailAccountId }, + ctx: { emailAccountId, provider }, parsedInput: { name, automate, @@ -133,7 +133,13 @@ export const createRuleAction = actionClient createRuleHistory({ rule, triggerType: "manual_creation" }), ); - after(() => updatePromptFileOnRuleCreated({ emailAccountId, rule })); + after(() => + updatePromptFileOnRuleCreated({ + emailAccountId, + provider, + rule, + }), + ); return { rule }; } catch (error) { diff --git a/apps/web/utils/ai/assistant/process-user-request.ts b/apps/web/utils/ai/assistant/process-user-request.ts index 14d481cdfc..f1bc04abfa 100644 --- a/apps/web/utils/ai/assistant/process-user-request.ts +++ b/apps/web/utils/ai/assistant/process-user-request.ts @@ -648,6 +648,7 @@ ${senderCategory || "No category"} for (const rule of createdRules.values()) { await updatePromptFileOnRuleCreated({ emailAccountId: emailAccount.id, + provider: emailAccount.account.provider, rule, }); } diff --git a/apps/web/utils/ai/rule/create-prompt-from-rule.test.ts b/apps/web/utils/ai/rule/create-prompt-from-rule.test.ts index 764197b73c..b815e5bf17 100644 --- a/apps/web/utils/ai/rule/create-prompt-from-rule.test.ts +++ b/apps/web/utils/ai/rule/create-prompt-from-rule.test.ts @@ -9,7 +9,7 @@ describe("generatePromptFromRule", () => { actions: [{ type: "ARCHIVE" }] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from "newsletter@example.com", archive', ); }); @@ -25,7 +25,7 @@ describe("generatePromptFromRule", () => { ], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from "support@company.com" and with subject containing "urgent" and with body containing "priority", label as "Important" and forward to manager@company.com', ); }); @@ -40,7 +40,7 @@ describe("generatePromptFromRule", () => { actions: [{ type: "LABEL", label: "Priority" }] as Action[], } as Rule & { actions: Action[]; categoryFilters: Category[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from senders in categories: Finance, Important, label as "Priority"', ); }); @@ -52,7 +52,7 @@ describe("generatePromptFromRule", () => { actions: [{ type: "ARCHIVE" }] as Action[], } as Rule & { actions: Action[]; categoryFilters: Category[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( "For emails from senders not in categories: Spam, archive", ); }); @@ -66,7 +66,7 @@ describe("generatePromptFromRule", () => { ] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails matching AI criteria: "contains a meeting request", create a draft and label as "Meeting"', ); }); @@ -86,7 +86,7 @@ describe("generatePromptFromRule", () => { ], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from "test@example.com", archive and label as "Test" and send a reply and send email to other@example.com and forward to forward@example.com and create a draft and mark as spam and call webhook at https://example.com/webhook', ); }); @@ -96,7 +96,9 @@ describe("generatePromptFromRule", () => { actions: [{ type: "ARCHIVE" }] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe("For all emails, archive"); + expect(createPromptFromRule(rule, "google")).toBe( + "For all emails, archive", + ); }); it("handles templated reply", () => { @@ -114,7 +116,7 @@ Alice`, ] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from "job@company.com", send a templated reply: "Hi {{name}},\n{{personalized reply}}\n\nI\'d love to set up a time to chat.\nAlice"', ); }); @@ -130,7 +132,7 @@ Alice`, ] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails with subject containing "newsletter", send a static reply: "Please unsubscribe me from your mailing list."', ); }); @@ -141,7 +143,7 @@ Alice`, actions: [{ type: "REPLY" }] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails matching AI criteria: "is a meeting request", send a reply', ); }); @@ -154,7 +156,7 @@ Alice`, actions: [{ type: "LABEL", label: "Sales" }] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from "sales@company.com" or with subject containing "invoice", label as "Sales"', ); }); @@ -166,7 +168,7 @@ Alice`, actions: [{ type: "LABEL", label: "Sales" }] as Action[], } as Rule & { actions: Action[] }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails from "sales@company.com" and with subject containing "invoice", label as "Sales"', ); }); @@ -180,7 +182,7 @@ Alice`, ] as Action[], } as Rule & { actions: Action[]; group: Group }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For all emails, label as "Receipt" and archive', ); }); @@ -192,7 +194,7 @@ Alice`, actions: [{ type: "ARCHIVE" }] as Action[], } as Rule & { actions: Action[]; group: Group }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails with subject containing "order", archive', ); }); @@ -205,7 +207,7 @@ Alice`, actions: [{ type: "ARCHIVE" }] as Action[], } as Rule & { actions: Action[]; group: Group }; - expect(createPromptFromRule(rule)).toBe( + expect(createPromptFromRule(rule, "google")).toBe( 'For emails with subject containing "order", archive', ); }); diff --git a/apps/web/utils/ai/rule/create-prompt-from-rule.ts b/apps/web/utils/ai/rule/create-prompt-from-rule.ts index b2eadb8726..187fbf277d 100644 --- a/apps/web/utils/ai/rule/create-prompt-from-rule.ts +++ b/apps/web/utils/ai/rule/create-prompt-from-rule.ts @@ -6,6 +6,7 @@ import { type Prisma, } from "@prisma/client"; import { createScopedLogger } from "@/utils/logger"; +import { getEmailTerminology } from "@/utils/terminology"; const logger = createScopedLogger("ai/rule/create-prompt-from-rule"); @@ -25,7 +26,11 @@ export type RuleWithRelations = Rule & { | null; }; -export function createPromptFromRule(rule: RuleWithRelations): string { +export function createPromptFromRule( + rule: RuleWithRelations, + provider: string, +): string { + const terminology = getEmailTerminology(provider); const conditions: string[] = []; const actions: string[] = []; @@ -55,7 +60,10 @@ export function createPromptFromRule(rule: RuleWithRelations): string { actions.push("archive"); break; case ActionType.LABEL: - if (action.label) actions.push(`label as "${action.label}"`); + if (action.label) + actions.push( + `${terminology.label.action.toLowerCase()} as "${action.label}"`, + ); break; case ActionType.REPLY: if (action.content) { diff --git a/apps/web/utils/ai/rule/generate-prompt-on-delete-rule.ts b/apps/web/utils/ai/rule/generate-prompt-on-delete-rule.ts index b2c32d12c9..f2e41cb4f7 100644 --- a/apps/web/utils/ai/rule/generate-prompt-on-delete-rule.ts +++ b/apps/web/utils/ai/rule/generate-prompt-on-delete-rule.ts @@ -20,7 +20,10 @@ export async function generatePromptOnDeleteRule({ existingPrompt: string; deletedRule: RuleWithRelations; }): Promise { - const deletedRulePrompt = createPromptFromRule(deletedRule); + const deletedRulePrompt = createPromptFromRule( + deletedRule, + emailAccount.account.provider, + ); if (!existingPrompt) return ""; if (!deletedRulePrompt) return ""; diff --git a/apps/web/utils/ai/rule/generate-prompt-on-update-rule.ts b/apps/web/utils/ai/rule/generate-prompt-on-update-rule.ts index a2153ebe89..ecca1e7498 100644 --- a/apps/web/utils/ai/rule/generate-prompt-on-update-rule.ts +++ b/apps/web/utils/ai/rule/generate-prompt-on-update-rule.ts @@ -16,8 +16,14 @@ export async function generatePromptOnUpdateRule({ currentRule: RuleWithRelations; updatedRule: RuleWithRelations; }): Promise { - const currentRulePrompt = createPromptFromRule(currentRule); - const updatedRulePrompt = createPromptFromRule(updatedRule); + const currentRulePrompt = createPromptFromRule( + currentRule, + emailAccount.account.provider, + ); + const updatedRulePrompt = createPromptFromRule( + updatedRule, + emailAccount.account.provider, + ); if (!existingPrompt) return ""; if (!updatedRulePrompt) return ""; diff --git a/apps/web/utils/rule/prompt-file.ts b/apps/web/utils/rule/prompt-file.ts index 18e270c050..d19834b926 100644 --- a/apps/web/utils/rule/prompt-file.ts +++ b/apps/web/utils/rule/prompt-file.ts @@ -7,12 +7,14 @@ import prisma from "@/utils/prisma"; export async function updatePromptFileOnRuleCreated({ emailAccountId, + provider, rule, }: { emailAccountId: string; + provider: string; rule: RuleWithRelations; }) { - const prompt = createPromptFromRule(rule); + const prompt = createPromptFromRule(rule, provider); await appendRulePrompt({ emailAccountId, rulePrompt: prompt }); } diff --git a/apps/web/utils/terminology.ts b/apps/web/utils/terminology.ts new file mode 100644 index 0000000000..c62f64add5 --- /dev/null +++ b/apps/web/utils/terminology.ts @@ -0,0 +1,42 @@ +import { isMicrosoftProvider } from "@/utils/email/provider-types"; + +interface EmailTerminology { + label: { + singular: string; + plural: string; + singularCapitalized: string; + pluralCapitalized: string; + action: string; + }; +} + +/** + * Get email terminology based on the provider + * Gmail uses "labels" while Outlook uses "categories" + */ +export function getEmailTerminology(provider: string): EmailTerminology { + const isOutlook = isMicrosoftProvider(provider); + + if (isOutlook) { + return { + label: { + singular: "category", + plural: "categories", + singularCapitalized: "Category", + pluralCapitalized: "Categories", + action: "Categorize", + }, + }; + } + + // Default to Gmail terminology + return { + label: { + singular: "label", + plural: "labels", + singularCapitalized: "Label", + pluralCapitalized: "Labels", + action: "Label", + }, + }; +}