Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -219,9 +216,41 @@ function RulesPromptForm({
}}
className={showExamples ? "sm:col-span-2" : ""}
>
<Label className="font-cal text-xl leading-7">
How your assistant should handle incoming emails
</Label>
<div className="flex items-center justify-between">
<Label className="font-cal text-xl leading-7">
How your assistant should handle incoming emails
</Label>

<Tooltip
contentComponent={
<div className="space-y-1">
<div className="font-medium">Formatting options:</div>
<div className="text-sm space-y-1">
<div>
<span className="font-mono font-bold text-blue-400">
*
</span>{" "}
for bullet points
</div>
<div>
<span className="font-mono font-bold text-blue-400">
@label
</span>{" "}
for labels
</div>
<div>
<span className="font-mono font-bold text-blue-400">
&gt; text
</span>{" "}
for quotes
</div>
</div>
</div>
}
>
<HelpCircleIcon className="h-5 w-5 cursor-pointer text-muted-foreground hover:text-foreground" />
</Tooltip>
</div>

<div className="mt-1.5 space-y-4">
<LoadingContent
Expand Down Expand Up @@ -348,7 +377,9 @@ function PureExamples({ onSelect }: { onSelect: (example: string) => void }) {
<div
className={`h-2 w-2 rounded-full ${color} mt-1.5 flex-shrink-0`}
/>
<span className="flex-1">{example}</span>
<span className="flex-1">
{convertLabelsToDisplay(example)}
</span>
</div>
</Button>
);
Expand Down
220 changes: 110 additions & 110 deletions apps/web/app/(app)/[emailAccountId]/assistant/examples.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion apps/web/app/(app)/onboarding/OnboardingEmailAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -127,7 +128,7 @@ ${defaultPrompt}`}
className="h-auto w-full justify-start text-wrap py-2 text-left"
onClick={() => addExamplePrompt(example)}
>
{example}
{convertLabelsToDisplay(example)}
</Button>
))}
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/components/assistant-chat/examples-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -144,7 +145,7 @@ export function ExamplesDialog({
</div>
)}
<span className="flex-1 whitespace-pre-wrap">
{example}
{convertLabelsToDisplay(example)}
</span>
</div>
</Button>
Expand Down
9 changes: 5 additions & 4 deletions apps/web/components/editor/extensions/LabelMention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
100 changes: 99 additions & 1 deletion apps/web/utils/mention.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
55 changes: 54 additions & 1 deletion apps/web/utils/mention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.9.8
v1.9.9
Loading