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 };
+ });