mirror of
https://github.com/Mail-0/Zero.git
synced 2026-07-01 08:16:28 +00:00
feat: add email subject generation functionality in EmailComposer and TRPC
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user