diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/settings/WritingStyleSetting.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/settings/WritingStyleSetting.tsx index 2a8267a8d1..9e609b5ef4 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/settings/WritingStyleSetting.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/settings/WritingStyleSetting.tsx @@ -24,8 +24,12 @@ import { saveWritingStyleBody, } from "@/utils/actions/user.validation"; import { saveWritingStyleAction } from "@/utils/actions/user"; +import { regenerateWritingStyleAction } from "@/utils/actions/assess"; import { Tiptap, type TiptapHandle } from "@/components/editor/Tiptap"; import { getActionErrorMessage } from "@/utils/error"; +import { Loader2, Sparkles } from "lucide-react"; + +const WRITING_STYLE_MAX_LENGTH = 2000; export function WritingStyleSetting() { const { data, isLoading, error } = useEmailAccountFull(); @@ -69,24 +73,48 @@ function WritingStyleDialog({ control, formState: { errors }, handleSubmit, + setValue, } = useForm({ defaultValues: { writingStyle: currentWritingStyle }, resolver: zodResolver(saveWritingStyleBody), }); + // --- REGENERATE ACTION --- + const { execute: generate, isExecuting: isGenerating } = useAction( + regenerateWritingStyleAction.bind(null, emailAccountId), + { + onSuccess: (data) => { + const rawStyle = data?.data?.writingStyle; + + if (rawStyle) { + // Truncate to ensure it passes the save validation schema + const safeStyle = rawStyle.slice(0, WRITING_STYLE_MAX_LENGTH); + + setValue("writingStyle", safeStyle); + if (editorRef.current?.editor) { + editorRef.current.editor.commands.setContent(safeStyle); + } + toastSuccess({ description: "Writing style regenerated!" }); + } else { + toastSuccess({ description: "Not enough data to generate style." }); + } + }, + onError: (error) => { + toastError({ description: getActionErrorMessage(error.error) }); + }, + }, + ); + // ------------------------- + const { execute, isExecuting } = useAction( saveWritingStyleAction.bind(null, emailAccountId), { onSuccess: () => { - toastSuccess({ - description: "Writing style saved!", - }); + toastSuccess({ description: "Writing style saved!" }); setOpen(false); }, onError: (error) => { - toastError({ - description: getActionErrorMessage(error.error), - }); + toastError({ description: getActionErrorMessage(error.error) }); }, onSettled: () => { mutate(); @@ -120,7 +148,7 @@ function WritingStyleDialog({ initialContent={field.value ?? ""} onChange={field.onChange} output="markdown" - className="prose prose-sm dark:prose-invert max-w-none [&_p.is-editor-empty:first-child::before]:pointer-events-none [&_p.is-editor-empty:first-child::before]:float-left [&_p.is-editor-empty:first-child::before]:h-0 [&_p.is-editor-empty:first-child::before]:text-muted-foreground [&_p.is-editor-empty:first-child::before]:content-[attr(data-placeholder)]" + className="prose prose-sm dark:prose-invert max-w-none" autofocus={false} preservePastedLineBreaks placeholder={`Typical Length: 2-3 sentences @@ -142,9 +170,26 @@ Notable Traits: {errors.writingStyle.message}

)} - + +
+ + + +
diff --git a/apps/web/utils/actions/assess.ts b/apps/web/utils/actions/assess.ts index 255e12ef45..a8fb5ca865 100644 --- a/apps/web/utils/actions/assess.ts +++ b/apps/web/utils/actions/assess.ts @@ -97,3 +97,55 @@ export const analyzeWritingStyleAction = actionClient return { success: true }; }); + +export const regenerateWritingStyleAction = actionClient + .metadata({ name: "regenerateWritingStyle" }) + .action(async ({ ctx: { emailAccountId, provider, logger } }) => { + const emailAccount = await prisma.emailAccount.findUnique({ + where: { id: emailAccountId }, + select: { + id: true, + userId: true, + email: true, + about: true, + multiRuleSelectionEnabled: true, + timezone: true, + calendarBookingLink: true, + user: { select: { aiProvider: true, aiModel: true, aiApiKey: true } }, + }, + }); + + if (!emailAccount) throw new SafeError("Email account not found"); + + const emailProvider = await createEmailProvider({ + emailAccountId, + provider, + logger, + }); + const sentMessages = await emailProvider.getSentMessages(20); + + const style = await aiAnalyzeWritingStyle({ + emails: sentMessages.map((email) => + getEmailForLLM(email, { extractReply: true }), + ), + emailAccount: { ...emailAccount, account: { provider } }, + }); + + if (!style) return { writingStyle: "" }; + + const writingStyle = [ + style.typicalLength ? `Typical Length: ${style.typicalLength}` : null, + style.formality ? `Formality: ${style.formality}` : null, + style.commonGreeting ? `Common Greeting: ${style.commonGreeting}` : null, + style.notableTraits.length + ? `Notable Traits: ${formatBulletList(style.notableTraits)}` + : null, + style.examples.length + ? `Examples: ${formatBulletList(style.examples)}` + : null, + ] + .filter(Boolean) + .join("\n"); + + return { writingStyle }; + });