From 5f9e780f37ed3d112f7f789e75c8cad1c229b907 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:39:16 +0200 Subject: [PATCH 1/2] examples with tagging --- .../assistant/RulesPrompt.tsx | 51 +++- .../[emailAccountId]/assistant/examples.ts | 220 +++++++++--------- .../onboarding/OnboardingEmailAssistant.tsx | 3 +- .../assistant-chat/examples-dialog.tsx | 3 +- .../editor/extensions/LabelMention.tsx | 9 +- apps/web/utils/mention.test.ts | 100 +++++++- apps/web/utils/mention.ts | 55 ++++- 7 files changed, 313 insertions(+), 128 deletions(-) diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx index f4c503a1df..15901572f9 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState, memo, useRef } from "react"; import { useLocalStorage } from "usehooks-ts"; -import { SparklesIcon, UserPenIcon } from "lucide-react"; +import { HelpCircleIcon, SparklesIcon, UserPenIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import useSWR from "swr"; @@ -15,10 +15,7 @@ import { SimpleRichTextEditor, type SimpleRichTextEditorRef, } from "@/components/editor/SimpleRichTextEditor"; -import { - saveRulesPromptBody, - type SaveRulesPromptBody, -} from "@/utils/actions/rule.validation"; +import type { SaveRulesPromptBody } from "@/utils/actions/rule.validation"; import type { RulesPromptResponse } from "@/app/api/user/rules/prompt/route"; import { LoadingContent } from "@/components/LoadingContent"; import { Tooltip } from "@/components/Tooltip"; @@ -27,6 +24,7 @@ import { examplePrompts, personas, } from "@/app/(app)/[emailAccountId]/assistant/examples"; +import { convertLabelsToDisplay } from "@/utils/mention"; import { PersonaDialog } from "@/app/(app)/[emailAccountId]/assistant/PersonaDialog"; import { useModal } from "@/hooks/useModal"; import { ProcessingPromptFileDialog } from "@/app/(app)/[emailAccountId]/assistant/ProcessingPromptFileDialog"; @@ -36,7 +34,6 @@ import { Label } from "@/components/ui/label"; import { SectionHeader } from "@/components/Typography"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/utils"; -import { Notice } from "@/components/Notice"; import { getActionTypeColor } from "@/app/(app)/[emailAccountId]/assistant/constants"; import { Skeleton } from "@/components/ui/skeleton"; import { useLabels } from "@/hooks/useLabels"; @@ -219,9 +216,41 @@ function RulesPromptForm({ }} className={showExamples ? "sm:col-span-2" : ""} > - +
+ + + +
Formatting options:
+
+
+ + * + {" "} + for bullet points +
+
+ + @label + {" "} + for labels +
+
+ + > text + {" "} + for quotes +
+
+
+ } + > + + +
void }) {
- {example} + + {convertLabelsToDisplay(example)} +
); diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts b/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts index bfdcc8e984..badc7cb29c 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts +++ b/apps/web/app/(app)/[emailAccountId]/assistant/examples.ts @@ -22,39 +22,39 @@ function formatPromptArray(promptArray: string[]): string { } const commonPrompts = [ - "Label urgent emails as 'Urgent'", - "Label emails from @mycompany.com addresses as 'Team'", + "Label urgent emails as @[Urgent]", + "Label emails from @mycompany.com addresses as @[Team]", ]; export const examplePrompts = [ ...commonPrompts, - 'Forward receipts to jane@accounting.com and label them "Receipt"', - 'Forward pitch decks to john@investing.com and label them "Pitch Deck"', + "Forward receipts to jane@accounting.com and label them @[Receipt]", + "Forward pitch decks to john@investing.com and label them @[Pitch Deck]", "Reply to cold emails by telling them to check out Inbox Zero. Then mark them as spam", - 'Label high priority emails as "High Priority"', + "Label high priority emails as @[High Priority]", "If a founder asks to set up a call, draft a reply with my calendar link: https://cal.com/example", "If someone asks to cancel a plan, draft a reply with the cancellation link: https://company.com/cancel", - 'If a founder sends me an investor update, label it "Investor Update" and archive it', - 'If someone pitches me their startup, label it as "Investing", archive it, and draft a friendly reply that I no longer have time to look at the email but if they get a warm intro, that\'s their best bet to get funding from me', + "If a founder sends me an investor update, label it @[Investor Update] and archive it", + "If someone pitches me their startup, label it as @[Investing], archive it, and draft a friendly reply that I no longer have time to look at the email but if they get a warm intro, that's their best bet to get funding from me", "If someone asks for a discount, reply with the discount code INBOX20", "If someone asks for help with Product or Company, draft a reply telling them I no longer work there, but they should reach out to Company for support", "Review any emails from questions@pr.com and see if any are about finance. If so, draft a friendly reply that answers the question", - 'If people ask me to speak at an event, label the email "Speaker Opportunity" and archive it', - 'Label customer emails as "Customer"', - 'Label legal documents as "Legal"', - 'Label server errors as "Error"', - 'Label Stripe emails as "Stripe"', + "If people ask me to speak at an event, label the email @[Speaker Opportunity] and archive it", + "Label customer emails as @[Customer]", + "Label legal documents as @[Legal]", + "Label server errors as @[Error]", + "Label Stripe emails as @[Stripe]", ]; const founderPromptArray = [ ...commonPrompts, "If someone asks to set up a call, draft a reply with my calendar link: https://cal.com/example", - 'Label customer feedback emails as "Customer Feedback"', - 'Label customer support emails as "Customer Support"', - 'Label emails from investors as "Investor"', - 'Label legal documents as "Legal"', - 'Label emails about travel as "Travel"', - 'Label recruitment related emails as "Hiring"', + "Label customer feedback emails as @[Customer Feedback]", + "Label customer support emails as @[Customer Support]", + "Label emails from investors as @[Investor]", + "Label legal documents as @[Legal]", + "Label emails about travel as @[Travel]", + "Label recruitment related emails as @[Hiring]", ]; export const personas = { @@ -69,7 +69,7 @@ export const personas = { label: "📹 Influencer", promptArray: [ ...commonPrompts, - `Label sponsorship inquiries as "Sponsorship" and draft a reply as follows: + `Label sponsorship inquiries as @[Sponsorship] and draft a reply as follows: --- Hey NAME, @@ -77,10 +77,10 @@ SENTENCE RELATED TO THEIR INQUIRY 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', + "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); @@ -90,15 +90,15 @@ I've attached my media kit and pricing. 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', + "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', + "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); @@ -109,19 +109,19 @@ I've attached my media kit and pricing. 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"', + "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"', + "Label emails about fund administration as @[Fund Admin]", + "Label emails about speaking at investment conferences as @[Speaking Opportunity]", ], get prompt() { return formatPromptArray(this.promptArray); @@ -138,15 +138,15 @@ I've attached my media kit and pricing. 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', + "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); @@ -156,15 +156,15 @@ I've attached my media kit and pricing. 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"', + "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', + "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); @@ -174,18 +174,18 @@ I've attached my media kit and pricing. 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"', + "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', + "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 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', + "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); @@ -195,14 +195,14 @@ I've attached my media kit and pricing. 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', + "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); @@ -212,18 +212,18 @@ I've attached my media kit and pricing. 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', + "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', + "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', + "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', + "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); @@ -233,25 +233,25 @@ I've attached my media kit and pricing. 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"', + "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"', + "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); @@ -260,14 +260,14 @@ I've attached my media kit and pricing. 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', + "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', + "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); @@ -276,7 +276,7 @@ I've attached my media kit and pricing. 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', + "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); diff --git a/apps/web/app/(app)/onboarding/OnboardingEmailAssistant.tsx b/apps/web/app/(app)/onboarding/OnboardingEmailAssistant.tsx index 4d349112d1..1b96747262 100644 --- a/apps/web/app/(app)/onboarding/OnboardingEmailAssistant.tsx +++ b/apps/web/app/(app)/onboarding/OnboardingEmailAssistant.tsx @@ -30,6 +30,7 @@ import { type RulesExamplesBody, } from "@/utils/actions/rule.validation"; import { examplePrompts } from "@/app/(app)/[emailAccountId]/assistant/examples"; +import { convertLabelsToDisplay } from "@/utils/mention"; import { useAccount } from "@/providers/EmailAccountProvider"; type RulesExamplesResponse = InferSafeActionFnResult< @@ -127,7 +128,7 @@ ${defaultPrompt}`} className="h-auto w-full justify-start text-wrap py-2 text-left" onClick={() => addExamplePrompt(example)} > - {example} + {convertLabelsToDisplay(example)} ))}
diff --git a/apps/web/components/assistant-chat/examples-dialog.tsx b/apps/web/components/assistant-chat/examples-dialog.tsx index ef137a5ff7..cd52e1d562 100644 --- a/apps/web/components/assistant-chat/examples-dialog.tsx +++ b/apps/web/components/assistant-chat/examples-dialog.tsx @@ -18,6 +18,7 @@ import { CheckCircle2Icon, } from "lucide-react"; import { personas } from "@/app/(app)/[emailAccountId]/assistant/examples"; +import { convertLabelsToDisplay } from "@/utils/mention"; import { Tooltip } from "@/components/Tooltip"; import { ButtonList } from "@/components/ButtonList"; import { parseAsStringEnum, useQueryState } from "nuqs"; @@ -144,7 +145,7 @@ export function ExamplesDialog({ )} - {example} + {convertLabelsToDisplay(example)} diff --git a/apps/web/components/editor/extensions/LabelMention.tsx b/apps/web/components/editor/extensions/LabelMention.tsx index 2953979680..dda52ad1c7 100644 --- a/apps/web/components/editor/extensions/LabelMention.tsx +++ b/apps/web/components/editor/extensions/LabelMention.tsx @@ -180,17 +180,18 @@ export const createLabelMentionExtension = (labels: UserLabel[]) => { // Find the label in our labels array const label = labels.find((l) => l.name === labelName); - if (!label) return false; + // Create mention node even if label doesn't exist yet + // This allows examples to work even when labels haven't been created in Gmail if (!silent) { const token = state.push("mention_open", "mention", 1); token.attrs = [ - ["id", label.id], - ["label", label.name], + ["id", label?.id || `placeholder-${labelName}`], + ["label", labelName], ]; const textToken = state.push("text", "", 0); - textToken.content = `@${label.name}`; + textToken.content = `@${labelName}`; state.push("mention_close", "mention", -1); } diff --git a/apps/web/utils/mention.test.ts b/apps/web/utils/mention.test.ts index 4952a2b0aa..2b70d7dfca 100644 --- a/apps/web/utils/mention.test.ts +++ b/apps/web/utils/mention.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { convertMentionsToLabels } from "./mention"; +import { convertMentionsToLabels, convertLabelsToDisplay } from "./mention"; describe("convertMentionsToLabels", () => { it("converts single mention to label", () => { @@ -110,3 +110,101 @@ describe("convertMentionsToLabels", () => { expect(convertMentionsToLabels(input)).toBe(expected); }); }); + +describe("convertLabelsToDisplay", () => { + it("converts single mention to quoted label", () => { + const input = "Label this email as @[Newsletter]"; + const expected = 'Label this email as "Newsletter"'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("converts multiple mentions to quoted labels", () => { + const input = "Label as @[Important] and @[Work] and archive"; + const expected = 'Label as "Important" and "Work" and archive'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles mentions with spaces in label names", () => { + const input = "Apply @[Very Important] and @[Work Project] labels"; + const expected = 'Apply "Very Important" and "Work Project" labels'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles mentions with special characters in label names", () => { + const input = "Label as @[Finance/Tax] and @[Client-A] and @[2024_Q1]"; + const expected = 'Label as "Finance/Tax" and "Client-A" and "2024_Q1"'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles mentions at the beginning of text", () => { + const input = "@[Newsletter] emails should be archived"; + const expected = '"Newsletter" emails should be archived'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles mentions at the end of text", () => { + const input = "Archive and label as @[Newsletter]"; + const expected = 'Archive and label as "Newsletter"'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles text with no mentions", () => { + const input = "Archive all newsletters automatically"; + const expected = "Archive all newsletters automatically"; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles empty string", () => { + const input = ""; + const expected = ""; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles mentions in multiline text", () => { + const input = `When I get a newsletter, archive it and label it as @[Newsletter] + + For urgent emails from company.com, label as @[Urgent] and forward to support@company.com`; + + const expected = `When I get a newsletter, archive it and label it as "Newsletter" + + For urgent emails from company.com, label as "Urgent" and forward to support@company.com`; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("preserves regular @ symbols that are not mentions", () => { + const input = "Forward to support@company.com and label as @[Support]"; + const expected = 'Forward to support@company.com and label as "Support"'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles malformed mentions gracefully", () => { + const input = "Label as @[Newsletter and @Missing] and @[Complete]"; + const expected = 'Label as "Newsletter and @Missing" and "Complete"'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles nested brackets in mentions", () => { + const input = "Label as @[Project [Alpha]] and continue"; + const expected = 'Label as "Project [Alpha]" and continue'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); + + it("handles mentions with numbers and symbols", () => { + const input = "Apply @[2024-Q1] and @[Client#123] labels"; + const expected = 'Apply "2024-Q1" and "Client#123" labels'; + + expect(convertLabelsToDisplay(input)).toBe(expected); + }); +}); diff --git a/apps/web/utils/mention.ts b/apps/web/utils/mention.ts index 05d9074a4b..44bfb58316 100644 --- a/apps/web/utils/mention.ts +++ b/apps/web/utils/mention.ts @@ -4,5 +4,58 @@ * receives clean label names without the mention syntax */ export function convertMentionsToLabels(promptFile: string): string { - return promptFile.replace(/@\[([^\]]+)\]/g, "$1"); + return processMentions(promptFile, (match) => match); +} + +/** + * Converts @[LABEL] format to "LABEL" for display in the UI + * This is the inverse of convertMentionsToLabels + */ +export function convertLabelsToDisplay(text: string): string { + return processMentions(text, (match) => `"${match}"`); +} + +/** + * Helper function to process mentions with proper bracket matching + */ +function processMentions( + text: string, + transformer: (match: string) => string, +): string { + let result = ""; + let i = 0; + + while (i < text.length) { + // Look for @[ + if (i < text.length - 1 && text[i] === "@" && text[i + 1] === "[") { + // Found start of mention, find the matching closing bracket + let bracketCount = 1; + let j = i + 2; + + while (j < text.length && bracketCount > 0) { + if (text[j] === "[") { + bracketCount++; + } else if (text[j] === "]") { + bracketCount--; + } + j++; + } + + if (bracketCount === 0) { + // Found matching closing bracket + const labelContent = text.slice(i + 2, j - 1); + result += transformer(labelContent); + i = j; + } else { + // No matching bracket found, treat as regular text + result += text[i]; + i++; + } + } else { + result += text[i]; + i++; + } + } + + return result; } From b94ed4b8f22f1004f7bbac1219121cc944df22e6 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:41:59 +0200 Subject: [PATCH 2/2] v1.9.9 --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b0376728d8..ecd7e10ccf 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v1.9.8 +v1.9.9 \ No newline at end of file