feat: add email subject generation functionality in EmailComposer and TRPC

This commit is contained in:
Aj Wazzan
2025-05-11 20:23:02 -07:00
parent 82e6661b53
commit 69bd73e705
3 changed files with 101 additions and 8 deletions

View File

@@ -104,11 +104,14 @@ export function EmailComposer({
const [draftId, setDraftId] = useState<string | null>(urlDraftId ?? null);
const [aiGeneratedMessage, setAiGeneratedMessage] = useState<string | null>(null);
const [aiIsLoading, setAiIsLoading] = useState(false);
const [isGeneratingSubject, setIsGeneratingSubject] = useState(false);
const trpc = useTRPC();
const { mutateAsync: aiCompose } = useMutation(trpc.ai.compose.mutationOptions());
const { mutateAsync: createDraft } = useMutation(trpc.drafts.create.mutationOptions());
const { mutateAsync: generateEmailSubject } = useMutation(
trpc.ai.generateEmailSubject.mutationOptions(),
);
useEffect(() => {
if (isComposeOpen === 'true' && toInputRef.current) {
toInputRef.current.focus();
@@ -355,6 +358,13 @@ export function EmailComposer({
}
};
const handleGenerateSubject = async () => {
setIsGeneratingSubject(true);
const { subject } = await generateEmailSubject({ message: editor.getText() });
setValue('subject', subject);
setIsGeneratingSubject(false);
};
useEffect(() => {
if (urlDraftId !== draftId) {
setDraftId(urlDraftId ?? null);
@@ -661,11 +671,31 @@ export function EmailComposer({
setHasUnsavedChanges(true);
}}
/>
<button
className=""
onClick={handleGenerateSubject}
disabled={isLoading || isGeneratingSubject}
>
<div className="flex items-center justify-center gap-2.5 pl-0.5">
<div className="flex h-5 items-center justify-center gap-1 rounded-sm">
{isGeneratingSubject ? (
<Loader className="h-3.5 w-3.5 animate-spin fill-black dark:fill-white" />
) : (
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
)}
</div>
</div>
</button>
</div>
{/* Message Content */}
<div className="relative -bottom-1 flex flex-col items-start justify-start gap-2 self-stretch border-t bg-[#FFFFFF] px-3 py-3 outline-white/5 dark:bg-[#202020]">
<div className="flex flex-col gap-2.5 self-stretch">
<div
className={cn(
'flex flex-col gap-2.5 self-stretch',
aiGeneratedMessage !== null ? 'blur-sm' : '',
)}
>
<EditorContent editor={editor} />
</div>
@@ -853,9 +883,8 @@ export function EmailComposer({
<button
className="flex h-7 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-md border border-[#8B5CF6] pl-1.5 pr-2 dark:bg-[#252525]"
onClick={async () => {
if (!toEmails.length || !subjectInput.trim()) {
toast.error('Please enter a recipient and subject');
return;
if (!subjectInput.trim()) {
await handleGenerateSubject();
}
setAiGeneratedMessage(null);
await handleAiGenerate();
@@ -894,7 +923,7 @@ export function EmailComposer({
<TooltipTrigger asChild>
<button
disabled
className="flex hidden h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 md:flex"
className="flex h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 md:flex"
>
{messageLength < 50 && <ShortStack className="h-3 w-3 fill-[#9A9A9A]" />}
{messageLength >= 50 && messageLength < 200 && (
@@ -970,7 +999,7 @@ const ContentPreview = ({
initial="initial"
animate="animate"
exit="exit"
className="absolute bottom-full right-0 z-30 w-[400px] overflow-hidden rounded-xl border bg-white shadow-md dark:bg-black"
className="dark:bg-subtleBlack absolute bottom-full right-0 z-30 w-[400px] overflow-hidden rounded-xl border bg-white p-1 shadow-md"
>
<div
className="max-h-60 min-h-[150px] overflow-y-auto rounded-md p-1 text-sm"

View File

@@ -105,6 +105,28 @@ export const compose = activeConnectionProcedure
};
});
export const generateEmailSubject = activeConnectionProcedure
.input(
z.object({
message: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
const { activeConnection } = ctx;
const { message } = input;
const writingStyleMatrix = await getWritingStyleMatrixForConnectionId({
connectionId: activeConnection.id,
c: ctx.c,
});
const subject = await generateSubject(message, writingStyleMatrix?.style as WritingStyleMatrix);
return {
subject,
};
});
const MessagePrompt = ({
from,
to,
@@ -196,3 +218,44 @@ const EmailAssistantPrompt = ({
return parts.join('\n\n');
};
const generateSubject = async (message: string, styleProfile?: WritingStyleMatrix | null) => {
const parts: string[] = [];
parts.push('# Email Subject Generation Task');
if (styleProfile) {
parts.push('## Style Profile');
parts.push(`\`\`\`json
${JSON.stringify(styleProfile, null, 2)}
\`\`\``);
}
parts.push('## Email Content');
parts.push(escapeXml(message));
parts.push('');
parts.push(
'Generate a concise, clear subject line that summarizes the main point of the email. The subject should be professional and under 100 characters.',
);
const { text } = await generateText({
model: openai('gpt-4o'),
messages: [
{
role: 'system',
content:
'You are an email subject line generator. Generate a concise, clear subject line that summarizes the main point of the email. The subject should be professional and under 100 characters.',
},
{
role: 'user',
content: parts.join('\n\n'),
},
],
maxTokens: 50,
temperature: 0.3,
frequencyPenalty: 0.1,
presencePenalty: 0.1,
maxRetries: 1,
});
return text.trim();
};

View File

@@ -1,8 +1,9 @@
import { compose, generateEmailSubject } from './compose';
import { generateSearchQuery } from './search';
import { router } from '../../trpc';
import { compose } from './compose';
export const aiRouter = router({
generateSearchQuery: generateSearchQuery,
compose: compose,
generateEmailSubject: generateEmailSubject,
});