email templates, drafts and theme working

This commit is contained in:
aadesh18 2026-02-19 16:43:34 -08:00
parent a6774861d6
commit 0e71470723
23 changed files with 339 additions and 676 deletions

View File

@ -30,7 +30,7 @@ export const POST = createSmartRouteHandler({
}
if (!validateToolNames(body.tools)) {
throw new StatusError(StatusError.BadRequest, `Invalid tool names in request. Valid tools: docs, sql-query, create-email-theme, create-email-template, create-email-draft, create-dashboard`);
throw new StatusError(StatusError.BadRequest, `Invalid tool names in request.`);
}
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY", "");

View File

@ -1,122 +1,6 @@
import { getChatAdapter } from "@/lib/ai-chat/adapter-registry";
import { selectModel } from "@/lib/ai/models";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString, yupUnion } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
import { generateText } from "ai";
import { InferType } from "yup";
const textContentSchema = yupObject({
type: yupString().oneOf(["text"]).defined(),
text: yupString().defined(),
});
const toolCallContentSchema = yupObject({
type: yupString().oneOf(["tool-call"]).defined(),
toolName: yupString().defined(),
toolCallId: yupString().defined(),
args: yupMixed().defined(),
argsText: yupString().defined(),
result: yupMixed().defined(),
});
const contentSchema = yupArray(yupUnion(textContentSchema, toolCallContentSchema)).defined();
const messageSchema = yupObject({
role: yupString().oneOf(["user", "assistant", "tool"]).defined(),
content: yupMixed().defined(),
});
// AI request timeout in milliseconds (2 minutes)
const AI_REQUEST_TIMEOUT_MS = 120_000;
export const POST = createSmartRouteHandler({
metadata: {
hidden: true,
},
request: yupObject({
auth: yupObject({
type: yupString().oneOf(["admin"]).defined(),
tenancy: adaptSchema,
}),
params: yupObject({
threadId: yupString().defined(),
}),
body: yupObject({
context_type: yupString().oneOf(["email-theme", "email-template", "email-draft"]).defined(),
messages: yupArray(messageSchema).defined().min(1),
}),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
content: contentSchema,
}).defined(),
}),
async handler({ body, params, auth: { tenancy } }) {
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY", "");
if (apiKey === "" || apiKey === "FORWARD_TO_PRODUCTION") {
throw new StatusError(
StatusError.InternalServerError,
"OpenRouter API key is not configured. Please set STACK_OPENROUTER_API_KEY."
);
}
const adapter = getChatAdapter(body.context_type, tenancy, params.threadId);
// Email generation benefits from a smarter, slower model; this route always has
// admin auth so isAuthenticated is always true
const model = selectModel("smart", "slow", true);
// content is typed as yup mixed — cast needed since it does not map to the AI
// SDK strict ModelMessage content typing, but the adapter guarantees a valid shape
const validatedMessages = body.messages.map((msg) => ({
role: msg.role,
content: msg.content,
})) as any;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), AI_REQUEST_TIMEOUT_MS);
try {
const result = await generateText({
model,
system: adapter.systemPrompt,
messages: validatedMessages,
tools: adapter.tools,
abortSignal: controller.signal,
});
const contentBlocks: InferType<typeof contentSchema> = [];
result.steps.forEach((step) => {
if (step.text) {
contentBlocks.push({ type: "text", text: step.text });
}
step.toolCalls.forEach((toolCall) => {
contentBlocks.push({
type: "tool-call",
toolName: toolCall.toolName,
toolCallId: toolCall.toolCallId,
args: toolCall.input,
argsText: JSON.stringify(toolCall.input),
result: "success",
});
});
});
return {
statusCode: 200,
bodyType: "json",
body: { content: contentBlocks },
};
} finally {
clearTimeout(timeoutId);
}
},
});
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
export const PATCH = createSmartRouteHandler({
metadata: {

View File

@ -1,23 +1,9 @@
import { selectModel } from "@/lib/ai/models";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { createOpenAI } from "@ai-sdk/openai";
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { generateText } from "ai";
// Mock mode sentinel value - when API key is not configured, we return mock responses
const MOCK_API_KEY_SENTINEL = "mock-openrouter-api-key";
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY", MOCK_API_KEY_SENTINEL);
const isMockMode = apiKey === MOCK_API_KEY_SENTINEL;
// Only create OpenAI client if not in mock mode
const openai = isMockMode ? null : createOpenAI({
apiKey,
baseURL: "https://openrouter.ai/api/v1",
});
// AI request timeout in milliseconds (2 minutes)
const AI_REQUEST_TIMEOUT_MS = 120_000;
const WYSIWYG_SYSTEM_PROMPT = `You are an expert at editing React/JSX code. Your task is to update a specific text string in the source code.
RULES:
@ -102,7 +88,7 @@ export const POST = createSmartRouteHandler({
updated_source: yupString().defined(),
}).defined(),
}),
async handler({ body }) {
async handler({ body }, fullReq) {
const {
source_code,
old_text,
@ -121,8 +107,10 @@ export const POST = createSmartRouteHandler({
};
}
// Mock mode: perform string replacement at the correct occurrence index without calling AI
if (isMockMode) {
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY", "");
// Mock mode: no API key configured — perform a simple string replacement without calling AI
if (apiKey === "") { //TODO have a special env variable for this
let replacedSource: string;
// Handle edge case: empty old_text can't be meaningfully replaced
@ -197,29 +185,15 @@ ${html_context.slice(0, 500)}
Please update the source code to change "${old_text}" to "${new_text}" at the specified location. Return ONLY the complete updated source code.
`;
// Model is configurable via env var; no default to surface missing config errors
const modelName = getEnvVariable("STACK_AI_MODEL");
// This route requires admin auth, so the caller is always authenticated.
// "smart" + "fast" is appropriate for surgical text-node replacement.
const model = selectModel("smart", "fast", /* isAuthenticated= */ true);
if (!openai) {
// This shouldn't happen since we check isMockMode above, but guard anyway
throw new Error("OpenAI client not initialized - STACK_OPENROUTER_API_KEY may be missing");
}
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), AI_REQUEST_TIMEOUT_MS);
let result;
try {
result = await generateText({
model: openai(modelName),
system: WYSIWYG_SYSTEM_PROMPT,
messages: [{ role: "user", content: userPrompt }],
abortSignal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}
const result = await generateText({
model,
system: WYSIWYG_SYSTEM_PROMPT,
messages: [{ role: "user", content: userPrompt }],
});
// Extract the updated source code from the response
let updatedSource = result.text.trim();

View File

@ -1,28 +0,0 @@
import { Tool } from "ai";
import { type Tenancy } from "../tenancies";
import { emailTemplateAdapter } from "./email-template-adapter";
import { emailThemeAdapter } from "./email-theme-adapter";
import { emailDraftAdapter } from "./email-draft-adapter";
export type ChatAdapterContext = {
tenancy: Tenancy,
threadId: string,
}
type ChatAdapter = {
systemPrompt: string,
tools: Record<string, Tool>,
}
type ContextType = "email-theme" | "email-template" | "email-draft";
const CHAT_ADAPTERS: Record<ContextType, (context: ChatAdapterContext) => ChatAdapter> = {
"email-theme": emailThemeAdapter,
"email-template": emailTemplateAdapter,
"email-draft": emailDraftAdapter,
};
export function getChatAdapter(contextType: ContextType, tenancy: Tenancy, threadId: string): ChatAdapter {
const adapter = CHAT_ADAPTERS[contextType];
return adapter({ tenancy, threadId });
}

View File

@ -1,64 +0,0 @@
import { tool } from "ai";
import { z } from "zod";
import { ChatAdapterContext } from "./adapter-registry";
const EMAIL_DRAFT_SYSTEM_PROMPT = `
Do not include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
You are an expert email copywriter and designer.
Your goal is to create high-converting, professional, and visually appealing email drafts.
PRINCIPLES:
- Compelling copywriting: Use clear, engaging language.
- Premium design: Use modern layouts and balanced spacing.
- Professional tone: Match the project's identity.
- Mobile responsiveness: Ensure drafts look good on all devices.
TECHNICAL RULES:
- YOU MUST WRITE A FULL REACT COMPONENT WHEN CALLING THE createEmailTemplate TOOL.
- Always include a <Subject />.
- Do NOT include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
- Use only tailwind classes for styling.
- Export 'EmailTemplate' component.
`;
export const emailDraftAdapter = (context: ChatAdapterContext) => ({
systemPrompt: EMAIL_DRAFT_SYSTEM_PROMPT,
tools: {
createEmailTemplate: tool({
description: CREATE_EMAIL_DRAFT_TOOL_DESCRIPTION(),
inputSchema: z.object({
content: z.string().describe("A react component that renders the email template"),
}),
}),
},
});
const CREATE_EMAIL_DRAFT_TOOL_DESCRIPTION = () => {
return `
Create a new email draft.
The email draft is a tsx file that is used to render the email content.
It must use react-email components.
It must export one thing:
- EmailTemplate: A function that renders the email draft
It must not import from any package besides "@react-email/components", "@stackframe/emails", and "arktype".
It uses tailwind classes for all styling.
Here is an example of a valid email draft:
\`\`\`tsx
import { Container } from "@react-email/components";
import { Subject, NotificationCategory, Props } from "@stackframe/emails";
export function EmailTemplate({ user, project }: Props) {
return (
<Container>
<Subject value={\`Hello \${user.displayName}!\`} />
<NotificationCategory value="Transactional" />
<div className="font-bold">Hi {user.displayName}!</div>
<br />
</Container>
);
}
\`\`\`
`;
};

View File

@ -1,86 +0,0 @@
import { tool } from "ai";
import { z } from "zod";
import { ChatAdapterContext } from "./adapter-registry";
const EMAIL_TEMPLATE_SYSTEM_PROMPT = `
Do not include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
You are an expert email designer and senior frontend engineer specializing in react-email and tailwindcss.
Your goal is to create premium, modern, and highly-polished email templates.
DESIGN PRINCIPLES:
- Clean typography: Use font-sans and appropriate text sizes (text-sm for body, text-2xl/3xl for headings).
- Balanced spacing: Use generous padding and margins (py-8, gap-4).
- Modern aesthetics: Use subtle borders, soft shadows (if supported/simulated), and professional color palettes.
- Mobile-first: Ensure designs look great on small screens.
- Clarity: The main call-to-action should be prominent.
TECHNICAL RULES:
- YOU MUST WRITE A FULL REACT COMPONENT WHEN CALLING THE createEmailTemplate TOOL.
- Always include a <Subject /> component.
- Always include a <NotificationCategory /> component.
- Do NOT include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
- Use only tailwind classes for styling.
- Export 'variablesSchema' using arktype.
- Export 'EmailTemplate' component.
- Define 'EmailTemplate.PreviewVariables' with realistic example data.
`;
export const emailTemplateAdapter = (context: ChatAdapterContext) => ({
systemPrompt: EMAIL_TEMPLATE_SYSTEM_PROMPT,
tools: {
createEmailTemplate: tool({
description: CREATE_EMAIL_TEMPLATE_TOOL_DESCRIPTION(context),
inputSchema: z.object({
content: z.string().describe("A react component that renders the email template"),
}),
}),
},
});
const CREATE_EMAIL_TEMPLATE_TOOL_DESCRIPTION = (context: ChatAdapterContext) => {
const currentEmailTemplate = context.tenancy.config.emails.templates[context.threadId];
return `
Create a new email template.
The email template is a tsx file that is used to render the email content.
It must use react-email components.
It must export two things:
- variablesSchema: An arktype schema for the email template props
- EmailTemplate: A function that renders the email template. You must set the PreviewVariables property to an object that satisfies the variablesSchema by doing EmailTemplate.PreviewVariables = { ...
It must not import from any package besides "@react-email/components", "@stackframe/emails", and "arktype".
It uses tailwind classes for all styling.
Here is an example of a valid email template:
\`\`\`tsx
import { type } from "arktype"
import { Container } from "@react-email/components";
import { Subject, NotificationCategory, Props } from "@stackframe/emails";
export const variablesSchema = type({
count: "number"
});
export function EmailTemplate({ user, variables }: Props<typeof variablesSchema.infer>) {
return (
<Container>
<Subject value={\`Hello \${user.displayName}!\`} />
<NotificationCategory value="Transactional" />
<div className="font-bold">Hi {user.displayName}!</div>
<br />
count is {variables.count}
</Container>
);
}
EmailTemplate.PreviewVariables = {
count: 10
} satisfies typeof variablesSchema.infer
\`\`\`
Here is the user's current email template:
\`\`\`tsx
${currentEmailTemplate.tsxSource}
\`\`\`
`;
};

View File

@ -1,68 +0,0 @@
import { tool } from "ai";
import { z } from "zod";
import { ChatAdapterContext } from "./adapter-registry";
export const emailThemeAdapter = (context: ChatAdapterContext) => ({
systemPrompt: `
You are an expert email designer and senior frontend engineer.
Your goal is to create premium, modern email themes that provide a consistent look and feel across all emails.
DESIGN PRINCIPLES:
- Professional layout: Use a clear container and appropriate padding.
- Consistent branding: Use professional colors and clean typography.
- Mobile responsiveness: Ensure the theme works well on all devices.
- Accessibility: Use semantic tags and readable font sizes.
TECHNICAL RULES:
- Export 'EmailTheme' component.
- Take 'children' as a prop and render it inside the main layout.
- Use <Tailwind> for styling.
- Ensure the layout is robust and follows email design best practices.
`,
tools: {
createEmailTheme: tool({
description: CREATE_EMAIL_THEME_TOOL_DESCRIPTION(context),
inputSchema: z.object({
content: z.string().describe("The content of the email theme"),
}),
}),
},
});
const CREATE_EMAIL_THEME_TOOL_DESCRIPTION = (context: ChatAdapterContext) => {
const currentEmailTheme = context.tenancy.config.emails.themes[context.threadId].tsxSource || "";
return `
Create a new email theme.
The email theme is a React component that is used to render the email theme.
It must use react-email components.
It must be exported as a function with name "EmailTheme".
It must take one prop, children, which is a React node.
It must not import from any package besides "@react-email/components".
It uses tailwind classes inside of the <Tailwind> tag.
Here is an example of a valid email theme:
\`\`\`tsx
import { Container, Head, Html, Tailwind } from '@react-email/components'
export function EmailTheme({ children }: { children: React.ReactNode }) {
return (
<Html>
<Head />
<Tailwind>
<Container>{children}</Container>
</Tailwind>
</Html>
)
}
\`\`\`
Here is the current email theme:
\`\`\`tsx
${currentEmailTheme}
\`\`\`
`;
};

View File

@ -173,70 +173,97 @@ Remember: You're here to help users succeed with Stack Auth. Be helpful but conc
`,
"email-wysiwyg-editor": `
## Context: Email Template Editor
Do not include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
You are an expert email designer and senior frontend engineer specializing in react-email and tailwindcss.
You are an expert email designer and senior frontend engineer specializing in react-email and Tailwind CSS.
Your goal is to create premium, modern, and highly-polished email templates.
**DESIGN PRINCIPLES:**
- Clean typography: Use font-sans and appropriate text sizes (text-sm for body, text-2xl/3xl for headings)
- Balanced spacing: Use generous padding and margins (py-8, gap-4)
- Modern aesthetics: Use subtle borders, soft shadows (if supported/simulated), and professional color palettes
- Mobile-first: Ensure designs look great on small screens
- Clarity: The main call-to-action should be prominent
The current source code will be provided in the conversation messages. When modifying existing code:
- Make only the changes the user asked for; preserve everything else exactly as-is
- If the user's request is ambiguous, make the change that best matches their intent from a UX perspective
- Do NOT add explanatory comments about what you changed
- If the user added whitespace at the very start or end of a text node, that was probably accidental ignore it
**TECHNICAL RULES:**
- YOU MUST WRITE A FULL REACT COMPONENT WHEN CALLING THE createEmailTemplate TOOL
- Always include a <Subject /> component
- Always include a <NotificationCategory /> component
- Do NOT include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those)
- Use only tailwind classes for styling
- Export 'variablesSchema' using arktype
- Export 'EmailTemplate' component
- Define 'EmailTemplate.PreviewVariables' with realistic example data
DESIGN PRINCIPLES:
- Clean typography: Use font-sans and appropriate text sizes (text-sm for body, text-2xl/3xl for headings).
- Balanced spacing: Use generous padding and margins (py-8, gap-4).
- Modern aesthetics: Use subtle borders, soft shadows (if supported/simulated), and professional color palettes.
- Mobile-first: Ensure designs look great on small screens.
- Clarity: The main call-to-action should be prominent.
RULES:
1. The component must NOT include <Html>, <Head>, <Body>, or <Preview> the email theme provides those wrappers.
2. Always include a <Subject /> component with a meaningful value.
3. Always include a <NotificationCategory /> component (e.g., "Transactional" or "Marketing").
4. Export \`variablesSchema\` using arktype to define any dynamic variables the template uses.
5. Export the component as \`EmailTemplate\`. It must accept \`Props<typeof variablesSchema.infer>\` as its props type.
6. Set \`EmailTemplate.PreviewVariables\` with realistic sample data matching the schema.
7. Import email components only from \`@react-email/components\`, schema types from \`arktype\`, and Stack Auth helpers from \`@stackframe/emails\` (Subject, NotificationCategory, Props).
8. EVERY component you use in JSX must be explicitly imported. If you use \`<Hr />\`, import \`Hr\`. If you use \`<Img />\`, import \`Img\`. Never use a component without importing it.
9. Use only Tailwind classes for styling no inline styles.
10. If the text is part of a template literal or JSX expression, only change the static text portion.
11. YOU MUST call the \`createEmailTemplate\` tool with the complete code. NEVER output code directly in the chat.
12. Output raw TSX source code NEVER HTML-encode angle brackets. Write \`<Container>\`, not \`&lt;Container&gt;\`.
13. NEVER use bare & in JSX text content it is invalid JSX and causes a build error. Use \`&amp;\` or \`{"&"}\` instead.
`,
"email-assistant-theme": `
## Context: Email Theme Creation
You are an expert email designer and senior frontend engineer.
Your goal is to create premium, modern email themes that provide a consistent look and feel across all emails.
**DESIGN PRINCIPLES:**
- Professional layout: Use a clear container and appropriate padding
- Consistent branding: Use professional colors and clean typography
- Mobile responsiveness: Ensure the theme works well on all devices
- Accessibility: Use semantic tags and readable font sizes
The current source code will be provided in the conversation messages. When modifying existing code:
- Make only the changes the user asked for; preserve everything else exactly as-is
- If the user's request is ambiguous, make the change that best matches their intent from a UX perspective
- Do NOT add explanatory comments about what you changed
- If the user added whitespace at the very start or end of a text node, that was probably accidental ignore it
**TECHNICAL RULES:**
- Export 'EmailTheme' component
- Take 'children' as a prop and render it inside the main layout
- Use <Tailwind> for styling
- Must not import from any package besides "@react-email/components"
- Ensure the layout is robust and follows email design best practices
- Use the createEmailTheme tool to return the complete theme code
DESIGN PRINCIPLES:
- Professional layout: Use a clear container and appropriate padding.
- Consistent branding: Use professional colors and clean typography.
- Mobile responsiveness: Ensure the theme works well on all devices.
- Accessibility: Use semantic tags and readable font sizes.
COMPONENT PROPS:
The renderer calls \`<EmailTheme>\` with exactly these props — do NOT invent additional ones:
\`\`\`tsx
type EmailThemeProps = {
children: React.ReactNode, // required — the email body content
unsubscribeLink?: string, // optional URL string — use as href={unsubscribeLink}, NEVER as a function call
}
\`\`\`
RULES:
1. Export the component as \`EmailTheme\` with the exact props above.
2. Must include <Html>, <Head>, and a <Tailwind> wrapper (themes are responsible for the full document structure).
3. Import ONLY from \`@react-email/components\` — no other packages are allowed.
4. EVERY component you use in JSX must be explicitly imported. If you use \`<Hr />\`, import \`Hr\`. Never use a component without importing it.
5. Use only Tailwind classes for styling no inline styles.
6. The layout must be robust, responsive, and compatible with major email clients.
7. If the text is part of a template literal or JSX expression, only change the static text portion.
8. YOU MUST call the \`createEmailTheme\` tool with the complete code. NEVER output code directly in the chat.
9. Output raw TSX source code NEVER HTML-encode angle brackets. Write \`<EmailTheme>\`, not \`&lt;EmailTheme&gt;\`.
10. NEVER use bare & in JSX text content it is invalid JSX and causes a build error. Use \`&amp;\` or \`{"&"}\` instead.
11. Do NOT pass a \`config\` prop to \`<Tailwind>\`. Use only standard Tailwind utility classes in \`className\` props.
12. JavaScript object literals use COMMAS to separate properties never semicolons. Only TypeScript types/interfaces use semicolons. Example: \`{ a: 1, b: 2 }\` NOT \`{ a: 1; b: 2 }\`.
`,
"email-assistant-draft": `
## Context: Email Draft Creation
Do not include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
You are an expert email copywriter and designer.
Your goal is to create high-converting, professional, and visually appealing email drafts.
**PRINCIPLES:**
- Compelling copywriting: Use clear, engaging language
- Premium design: Use modern layouts and balanced spacing
- Professional tone: Match the project's identity
- Mobile responsiveness: Ensure drafts look good on all devices
PRINCIPLES:
- Compelling copywriting: Use clear, engaging language.
- Premium design: Use modern layouts and balanced spacing.
- Professional tone: Match the project's identity.
- Mobile responsiveness: Ensure drafts look good on all devices.
**TECHNICAL RULES:**
- YOU MUST WRITE A FULL REACT COMPONENT WHEN CALLING THE createEmailDraft TOOL
- Always include a <Subject /> component
- Do NOT include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those)
- Use only tailwind classes for styling
- Export 'EmailTemplate' component
TECHNICAL RULES:
- YOU MUST WRITE A FULL REACT COMPONENT WHEN CALLING THE createEmailTemplate TOOL.
- Always include a <Subject />.
- Do NOT include <Html>, <Head>, <Body>, or <Preview> components (the theme provides those).
- Use only tailwind classes for styling.
- Export 'EmailTemplate' component.
The current source code will be provided in the conversation messages.
`,
"create-dashboard": `

View File

@ -14,75 +14,38 @@ import { z } from "zod";
*/
export function createEmailDraftTool(auth: SmartRequestAuth | null) {
return tool({
description: `Create a new email draft for Stack Auth.
**What is an Email Draft?**
An email draft is a simpler version of an email template, without variable schemas. It's used for one-off emails or quick email creation.
**Requirements:**
- Must use @react-email/components for email components
- Can import from @stackframe/emails for Stack Auth-specific utilities
- Must export ONE thing: \`EmailTemplate\` function component
- Must include Subject and NotificationCategory components
- Uses Tailwind classes for all styling
- Can access user and project data via Props
**Differences from Email Templates:**
- No variablesSchema required
- No custom variables (only user and project data)
- No PreviewVariables needed
- Simpler for one-off or standard emails
**Structure:**
1. Import required components
2. Define EmailTemplate function component using Props type
3. Include Subject (can use user data)
4. Include NotificationCategory
5. Add email content using react-email components
**Example Valid Email Draft:**
description: `
Create a new email draft.
The email draft is a tsx file that is used to render the email content.
It must use react-email components.
It must export one thing:
- EmailTemplate: A function that renders the email draft
It must not import from any package besides "@react-email/components", "@stackframe/emails", and "arktype".
It uses tailwind classes for all styling.
The email must include <Html>, <Head />, <Preview />, <Tailwind>, <Body>, and <Container> in the correct hierarchy.
Do not use any Tailwind classes that require style injection (e.g., hover:, focus:, active:, group-hover:, media queries, dark:, etc.). Only use inlineable Tailwind utilities.
The <Head /> component must be rendered inside <Tailwind> to support Tailwind style injection
Here is an example of a valid email draft:
\`\`\`tsx
import { Container, Text, Button } from "@react-email/components";
import { Container } from "@react-email/components";
import { Subject, NotificationCategory, Props } from "@stackframe/emails";
export function EmailTemplate({ user, project }: Props) {
return (
<Container>
<Subject value={\`Welcome to \${project.displayName}, \${user.displayName}!\`} />
<Subject value={\`Hello \${user.displayName}!\`} />
<NotificationCategory value="Transactional" />
<Text className="text-2xl font-bold">
Welcome, {user.displayName}!
</Text>
<Text>
Thank you for joining {project.displayName}. We're excited to have you here.
</Text>
<Text>
Get started by visiting your dashboard and exploring the features.
</Text>
<Button href="https://example.com/dashboard" className="bg-blue-600 text-white px-4 py-2 rounded">
Go to Dashboard
</Button>
<div className="font-bold">Hi {user.displayName}!</div>
<br />
</Container>
);
}
\`\`\`
**Guidelines:**
- Keep content clear and focused
- Use appropriate tone
- Personalize with user and project data
- Include clear call-to-actions when needed
- Make it mobile-responsive
- Use email-safe styling
**Output:**
Return the COMPLETE draft code including all imports and component definition.`,
The user's current email draft can be found in the conversation messages.
`,
inputSchema: z.object({
content: z.string().describe("The complete email draft code as a TypeScript React component"),
content: z.string().describe("A react component that renders the email template"),
}),
// No execute function - the tool call is returned to the caller
});

View File

@ -12,83 +12,51 @@ import { z } from "zod";
*/
export function createEmailTemplateTool(auth: SmartRequestAuth | null) {
return tool({
description: `Create a new email template for Stack Auth.
description: `
Create a new email template.
The email template is a tsx file that is used to render the email content.
It must use react-email components.
It must export two things:
- variablesSchema: An arktype schema for the email template props
- EmailTemplate: A function that renders the email template. You must set the PreviewVariables property to an object that satisfies the variablesSchema by doing EmailTemplate.PreviewVariables = { ...
It must not import from any package besides "@react-email/components", "@stackframe/emails", and "arktype".
It uses tailwind classes for all styling.
The user's current email template will be provided in the conversation messages.
The email must include <Html>, <Head />, <Preview />, <Tailwind>, <Body>, and <Container> in the correct hierarchy.
Do not use any Tailwind classes that require style injection (e.g., hover:, focus:, active:, group-hover:, media queries, dark:, etc.). Only use inlineable Tailwind utilities.
The <Head /> component must be rendered inside <Tailwind> to support Tailwind style injection
**What is an Email Template?**
An email template is a complete email with content, variables, and metadata. It defines the structure and content of a specific type of email (e.g., welcome email, password reset, notification).
**Requirements:**
- Must use @react-email/components for email components
- Can import from @stackframe/emails for Stack Auth-specific utilities
- Can import from arktype for schema validation
- Must export TWO things:
1. \`variablesSchema\`: An arktype schema defining template variables
2. \`EmailTemplate\`: A function component that renders the email
- EmailTemplate must set PreviewVariables property with sample data
- Must use Props<typeof variablesSchema.infer> as the component props type
- Must include Subject and NotificationCategory components
- Uses Tailwind classes for all styling
**Structure:**
1. Import required components and types
2. Define variablesSchema using arktype
3. Define EmailTemplate function component
4. Include Subject (dynamic or static)
5. Include NotificationCategory (e.g., "Transactional", "Marketing")
6. Add email content using react-email components
7. Set EmailTemplate.PreviewVariables
**Example Valid Email Template:**
Here is an example of a valid email template:
\`\`\`tsx
import { type } from "arktype"
import { Container, Text, Button } from "@react-email/components";
import { Container } from "@react-email/components";
import { Subject, NotificationCategory, Props } from "@stackframe/emails";
export const variablesSchema = type({
actionUrl: "string",
expiresInHours: "number"
count: "number"
});
export function EmailTemplate({ user, variables }: Props<typeof variablesSchema.infer>) {
return (
<Container>
<Subject value={\`Action Required, \${user.displayName}!\`} />
<Subject value={\`Hello \${user.displayName}!\`} />
<NotificationCategory value="Transactional" />
<Text className="text-lg font-bold">
Hi {user.displayName}!
</Text>
<Text>
Please complete your action within {variables.expiresInHours} hours.
</Text>
<Button href={variables.actionUrl} className="bg-blue-600 text-white px-4 py-2 rounded">
Take Action
</Button>
<div className="font-bold">Hi {user.displayName}!</div>
<br />
count is {variables.count}
</Container>
);
}
EmailTemplate.PreviewVariables = {
actionUrl: "https://example.com/action",
expiresInHours: 24
} satisfies typeof variablesSchema.infer;
count: 10
} satisfies typeof variablesSchema.infer
\`\`\`
**Guidelines:**
- Make content clear, concise, and actionable
- Use appropriate tone for the email type
- Include all necessary information
- Add clear call-to-action buttons when needed
- Use user data (user.displayName, user.primaryEmail, etc.) to personalize
- Make it mobile-responsive
- Use email-safe styling
**Output:**
Return the COMPLETE template code including all imports, schema, component, and PreviewVariables.`,
The user's current email template can be found in the conversation messages.
`,
inputSchema: z.object({
content: z.string().describe("The complete email template code as a TypeScript React component with schema"),
content: z.string().describe("A react component that renders the email template"),
}),
// No execute function - the tool call is returned to the caller
});

View File

@ -12,55 +12,60 @@ import { z } from "zod";
*/
export function createEmailThemeTool(auth: SmartRequestAuth | null) {
return tool({
description: `Create a new email theme for Stack Auth emails.
description: `
Create a new email theme.
**What is an Email Theme?**
An email theme is a React component that wraps all email content, providing consistent structure, layout, and styling across all emails.
The email theme is a React component that wraps all emails with a consistent layout.
**Requirements:**
- Must use @react-email/components (no other imports allowed)
- Must be exported as a function named "EmailTheme"
- Must accept one prop: children (React.ReactNode)
- Must use Tailwind classes inside <Tailwind> tag
- Must include Html, Head, and appropriate container elements
- Should be responsive and compatible with major email clients
**Structure:**
1. Html wrapper
2. Head (for meta tags)
3. Tailwind wrapper (for styling)
4. Container/layout elements
5. {children} placeholder for email content
**Example Valid Email Theme:**
EXACT PROP SIGNATURE (do not change or add props):
\`\`\`tsx
import { Container, Head, Html, Tailwind } from '@react-email/components'
type EmailThemeProps = {
children: React.ReactNode, // required — the email body
unsubscribeLink?: string, // optional URL string — use as href={unsubscribeLink}, NOT as a function call
}
\`\`\`
export function EmailTheme({ children }: { children: React.ReactNode }) {
Other requirements:
- Must include \`<Html>\`, \`<Head>\`, and a \`<Tailwind>\` wrapper (the theme owns the full document)
- Import ONLY from \`@react-email/components\` — no other packages
- Use standard Tailwind utility classes in \`className\` props — do NOT pass a \`config\` prop to \`<Tailwind>\`
- EVERY component used in JSX must be explicitly imported
- JavaScript object literals use COMMAS between properties, never semicolons
The user's current email theme can be found in the conversation messages.
Here is an example of a valid email theme:
\`\`\`tsx
import { Body, Container, Head, Hr, Html, Link, Section, Text, Tailwind } from '@react-email/components'
export function EmailTheme({ children, unsubscribeLink }: { children: React.ReactNode, unsubscribeLink?: string }) {
return (
<Html>
<Head />
<Tailwind>
<Container className="bg-white p-8 rounded-lg">
{children}
</Container>
<Body className="bg-gray-50 font-sans">
<Container className="mx-auto max-w-[600px] py-8 px-4">
<Section className="bg-white rounded-lg shadow-sm p-8">
{children}
</Section>
<Section className="mt-6 text-center">
<Hr className="border-gray-200 mb-4" />
{unsubscribeLink && (
<Text className="text-xs text-gray-400">
<Link href={unsubscribeLink} className="text-gray-400 underline">Unsubscribe</Link>
</Text>
)}
</Section>
</Container>
</Body>
</Tailwind>
</Html>
)
}
\`\`\`
**Guidelines:**
- Keep it simple and focused on layout/structure
- Use neutral, professional styling that works for various email types
- Ensure good spacing and readability
- Make it mobile-responsive
- Test compatibility with email clients (use email-safe CSS)
**Output:**
Return the COMPLETE theme code as a TypeScript React component. Include all imports and the full component definition.`,
`,
inputSchema: z.object({
content: z.string().describe("The complete email theme code as a TypeScript React component"),
content: z.string().describe("The content of the email theme"),
}),
// No execute function - the tool call is returned to the caller
});

View File

@ -11,5 +11,3 @@ STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50
NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=true
STACK_FEATUREBASE_JWT_SECRET=secret-value
STACK_OPENAI_API_KEY=mock_openai_api_key

View File

@ -17,7 +17,6 @@
"lint": "eslint ."
},
"dependencies": {
"@ai-sdk/openai": "^3.0.25",
"@ai-sdk/react": "^3.0.72",
"@assistant-ui/react": "^0.10.24",
"@assistant-ui/react-ai-sdk": "^0.10.14",

View File

@ -10,11 +10,13 @@ import { ToolCallContent, createChatAdapter, createHistoryAdapter } from "@/comp
import { EmailDraftUI } from "@/components/vibe-coding/draft-tool-components";
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useParams } from "next/navigation";
import { AppEnabledGuard } from "../../app-enabled-guard";
import { useAdminApp } from "../../use-admin-app";
export default function PageClient({ draftId }: { draftId: string }) {
const stackAdminApp = useAdminApp();
const { projectId } = useParams() as { projectId: string };
const { setNeedConfirm } = useRouterConfirm();
const [saveAlert, setSaveAlert] = useState<{
variant: "destructive" | "success",
@ -162,7 +164,7 @@ export default function PageClient({ draftId }: { draftId: string }) {
chatComponent={
<AssistantChat
historyAdapter={createHistoryAdapter(stackAdminApp, draftId)}
chatAdapter={createChatAdapter(stackAdminApp, draftId, "email-draft", handleToolUpdate)}
chatAdapter={createChatAdapter(projectId, draftId, "email-draft", handleToolUpdate, () => currentCode)}
toolComponents={<EmailDraftUI setCurrentCode={setCurrentCode} />}
useOffWhiteLightMode
/>

View File

@ -18,12 +18,14 @@ import { ToolCallContent } from "@/components/vibe-coding/chat-adapters";
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "next/navigation";
import { AppEnabledGuard } from "../../app-enabled-guard";
import { PageLayout } from "../../page-layout";
import { useAdminApp } from "../../use-admin-app";
export default function PageClient(props: { templateId: string }) {
const stackAdminApp = useAdminApp();
const { projectId } = useParams() as { projectId: string };
const templates = stackAdminApp.useEmailTemplates();
const { setNeedConfirm } = useRouterConfirm();
const templateFromHook = templates.find((t) => t.id === props.templateId);
@ -270,7 +272,7 @@ export default function PageClient(props: { templateId: string }) {
}
chatComponent={
<AssistantChat
chatAdapter={createChatAdapter(stackAdminApp, template.id, "email-template", handleCodeUpdate)}
chatAdapter={createChatAdapter(projectId, template.id, "email-template", handleCodeUpdate, () => currentCode)}
historyAdapter={createHistoryAdapter(stackAdminApp, template.id)}
toolComponents={<EmailTemplateUI setCurrentCode={setCurrentCode} />}
useOffWhiteLightMode

View File

@ -12,12 +12,14 @@ import {
import { previewTemplateSource } from "@stackframe/stack-shared/dist/helpers/emails";
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { AppEnabledGuard } from "../../app-enabled-guard";
import { useAdminApp } from "../../use-admin-app";
export default function PageClient({ themeId }: { themeId: string }) {
const stackAdminApp = useAdminApp();
const { projectId } = useParams() as { projectId: string };
const theme = stackAdminApp.useEmailTheme(themeId);
const { setNeedConfirm } = useRouterConfirm();
const [currentCode, setCurrentCode] = useState(theme.tsxSource);
@ -124,7 +126,7 @@ export default function PageClient({ themeId }: { themeId: string }) {
}
chatComponent={
<AssistantChat
chatAdapter={createChatAdapter(stackAdminApp, themeId, "email-theme", handleThemeUpdate)}
chatAdapter={createChatAdapter(projectId, themeId, "email-theme", handleThemeUpdate, () => currentCode)}
historyAdapter={createHistoryAdapter(stackAdminApp, themeId)}
toolComponents={<EmailThemeUI setCurrentCode={setCurrentCode} />}
useOffWhiteLightMode

View File

@ -16,7 +16,7 @@ export async function POST(req: Request) {
}
const user = await stackServerApp.getUser({ or: "redirect" });
const { accessToken } = await user.getAuthJson();
const accessToken = await user.getAccessToken();
// Check if the user has admin access to the requested project
let hasProjectAccess = false;

View File

@ -0,0 +1,115 @@
import { getPublicEnvVar } from "@/lib/env";
import { stackServerApp } from "@/stack";
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
/**
* Sanitizes AI-generated JSX/TSX code before it is applied to the email renderer.
*
* Handles four common model output issues:
* 1. Markdown code fences (```tsx ... ```) wrapping the output despite instructions
* 2. HTML-encoded angle brackets (&lt;Component&gt; instead of <Component>)
* 3. Bare & in JSX text content (invalid JSX; must be &amp; or {"&"})
* 4. Semicolons used as property separators in JS object literals instead of commas
* (the AI confuses TypeScript interface syntax with JS object syntax).
* TypeScript also accepts commas in interfaces/types, so replacing ; , is always safe.
*/
function sanitizeGeneratedCode(code: string): string {
let result = code.trim();
// Strip markdown code fences if the model added them despite instructions.
// Handles ```tsx ... ``` and also plain ``` ... ```.
if (result.startsWith("```")) {
const lines = result.split("\n");
lines.shift(); // remove opening ```tsx or similar
if (lines[lines.length - 1]?.trim() === "```") {
lines.pop(); // remove closing ```
}
result = lines.join("\n").trim();
}
// Decode common HTML entities that models sometimes emit inside code.
// This fixes things like `&amp;&amp;` (should be `&&`) and `&lt;Container&gt;`.
// Only decodes the entities we expect in generated TSX.
result = result
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&amp;/g, "&");
// Fix the common model mistake of using `;` as a property separator in object literals.
// Replace `;` with `,` only when it looks like `key: value;` followed by another `key:`.
// This avoids touching for-loops and other valid `;` usage.
result = result.replace(/;(\s*\n\s*[A-Za-z_$][\w$]*\s*:)/g, ",$1");
return result;
}
export async function POST(req: Request) {
const payload = await req.json() as {
projectId: string,
systemPrompt: string,
tools: string[],
messages: unknown[],
quality?: string,
speed?: string,
};
const { projectId, systemPrompt, tools, messages, quality = "smartest", speed = "fast" } = payload;
const user = await stackServerApp.getUser({ or: "redirect" });
const accessToken = await user.getAccessToken();
const projects = await user.listOwnedProjects();
const hasProjectAccess = projects.some((p) => p.id === projectId);
if (!hasProjectAccess || !accessToken) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 403,
headers: { "content-type": "application/json" },
});
}
const backendBaseUrl =
getPublicEnvVar("NEXT_PUBLIC_SERVER_STACK_API_URL") ??
getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ??
throwErr("Backend API URL is not configured (NEXT_PUBLIC_STACK_API_URL)");
const backendResponse = await fetch(
`${backendBaseUrl}/api/latest/ai/query/generate`,
{
method: "POST",
headers: {
"content-type": "application/json",
"x-stack-access-type": "admin",
"x-stack-project-id": projectId,
"x-stack-admin-access-token": accessToken, //TODO not entirely sure
},
body: JSON.stringify({ quality, speed, systemPrompt, tools, messages }),
}
);
if (!backendResponse.ok) {
const error = await backendResponse.json().catch(() => ({ error: "Unknown error" }));
return new Response(JSON.stringify(error), {
status: backendResponse.status,
headers: { "content-type": "application/json" },
});
}
const result = await backendResponse.json() as { content: Array<{ type: string, args?: { content?: string, [key: string]: unknown }, [key: string]: unknown }> };
const sanitized = {
content: result.content.map((item) => {
if (item.type === "tool-call" && typeof item.args?.content === "string") {
return { ...item, args: { ...item.args, content: sanitizeGeneratedCode(item.args.content) } };
}
return item;
}),
};
return new Response(JSON.stringify(sanitized), {
status: 200,
headers: { "content-type": "application/json" },
});
}

View File

@ -12,51 +12,62 @@ const isToolCall = (content: { type: string }): content is ToolCallContent => {
return content.type === "tool-call";
};
const CONTEXT_MAP = {
"email-theme": { systemPrompt: "email-assistant-theme", tools: ["create-email-theme"] },
"email-template": { systemPrompt: "email-wysiwyg-editor", tools: ["create-email-template"] },
"email-draft": { systemPrompt: "email-assistant-draft", tools: ["create-email-draft"] },
} as const;
export function createChatAdapter(
adminApp: StackAdminApp,
projectId: string,
threadId: string,
contextType: "email-theme" | "email-template" | "email-draft",
onToolCall: (toolCall: ToolCallContent) => void
onToolCall: (toolCall: ToolCallContent) => void,
getCurrentSource?: () => string,
): ChatModelAdapter {
return {
async run({ messages, abortSignal }) {
try {
const formattedMessages = [];
for (const msg of messages) {
// Separate tool calls from other content
const toolCalls = msg.content.filter(isToolCall);
const nonToolContent = msg.content.filter(c => !isToolCall(c));
// Only add the message if it has non-tool content
if (nonToolContent.length > 0) {
formattedMessages.push({
role: msg.role,
content: nonToolContent
});
const textContent = msg.content.filter(c => !isToolCall(c));
if (textContent.length > 0) {
formattedMessages.push({ role: msg.role, content: textContent });
}
// Add tool results as separate messages
toolCalls.forEach(toolCall => {
formattedMessages.push({
role: "tool",
content: [{
type: "tool-result",
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
result: toolCall.result,
}],
});
});
}
const currentSource = getCurrentSource?.() ?? "";
const contextMessages: Array<{ role: "user" | "assistant", content: string }> = currentSource ? [
{ role: "user", content: `Here is the current source:\n\`\`\`tsx\n${currentSource}\n\`\`\`` },
{ role: "assistant", content: "Got it. What would you like to change?" },
] : [];
const { systemPrompt, tools } = CONTEXT_MAP[contextType];
const response = await fetch("/api/email-ai", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
projectId,
systemPrompt,
tools: [...tools],
messages: [...contextMessages, ...formattedMessages],
}),
signal: abortSignal,
});
if (!response.ok) {
throw new Error(`AI request failed: ${response.status}`);
}
const response = await adminApp.sendChatMessage(threadId, contextType, formattedMessages, abortSignal);
if (response.content.some(isToolCall)) {
const toolCall = response.content.find(isToolCall);
const result: { content: ChatContent } = await response.json();
if (result.content.some(isToolCall)) {
const toolCall = result.content.find(isToolCall);
if (toolCall) {
onToolCall(toolCall);
}
}
return {
content: response.content,
};
return { content: result.content };
} catch (error) {
if (abortSignal.aborted) {
return {};

View File

@ -11,7 +11,7 @@ export const EmailDraftUI = ({ setCurrentCode }: EmailDraftUIProps) => {
{ content: string },
"success"
>({
toolName: "createEmailTemplate",
toolName: "createEmailDraft",
render: ({ args }) => {
return (
<Card className="flex items-center gap-2 p-4 justify-between">

View File

@ -420,28 +420,6 @@ export class StackAdminInterface extends StackServerInterface {
);
}
async sendChatMessage(
threadId: string,
contextType: "email-theme" | "email-template" | "email-draft",
messages: Array<{ role: string, content: any }>,
abortSignal?: AbortSignal,
): Promise<{ content: ChatContent }> {
const response = await this.sendAdminRequest(
`/internal/ai-chat/${threadId}`,
{
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ context_type: contextType, messages }),
signal: abortSignal,
},
null,
);
return await response.json();
}
async saveChatMessage(threadId: string, message: any): Promise<void> {
await this.sendAdminRequest(
`/internal/ai-chat/${threadId}`,

View File

@ -25,7 +25,6 @@ import { clientVersion, createCache, getBaseUrl, getDefaultExtraRequestHeaders,
import { _StackServerAppImplIncomplete } from "./server-app-impl";
import { CompleteConfig, EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema";
import { ChatContent } from "@stackframe/stack-shared/dist/interface/admin-interface";
import type { EditableMetadata } from "@stackframe/stack-shared/dist/utils/jsx-editable-transpiler";
import { branchConfigSourceSchema } from "@stackframe/stack-shared/dist/schema-fields";
import * as yup from "yup";
@ -629,16 +628,6 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
}
await this._adminEmailDraftsCache.refresh([]);
}
async sendChatMessage(
threadId: string,
contextType: "email-theme" | "email-template" | "email-draft",
messages: Array<{ role: string, content: any }>,
abortSignal?: AbortSignal,
): Promise<{ content: ChatContent }> {
return await this._interface.sendChatMessage(threadId, contextType, messages, abortSignal);
}
async saveChatMessage(threadId: string, message: any): Promise<void> {
await this._interface.saveChatMessage(threadId, message);
}

View File

@ -1,4 +1,3 @@
import { ChatContent } from "@stackframe/stack-shared/dist/interface/admin-interface";
import { AnalyticsQueryOptions, AnalyticsQueryResponse } from "@stackframe/stack-shared/dist/interface/crud/analytics";
import type { AdminGetSessionReplayChunkEventsResponse, AdminGetSessionReplayAllEventsResponse } from "@stackframe/stack-shared/dist/interface/crud/session-replays";
import type { Transaction, TransactionType } from "@stackframe/stack-shared/dist/interface/crud/transactions";
@ -95,13 +94,6 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
createEmailTheme(displayName: string): Promise<{ id: string }>,
updateEmailTheme(id: string, tsxSource: string): Promise<void>,
deleteEmailTheme(id: string): Promise<void>,
sendChatMessage(
threadId: string,
contextType: "email-theme" | "email-template" | "email-draft",
messages: Array<{ role: string, content: any }>,
abortSignal?: AbortSignal,
): Promise<{ content: ChatContent }>,
saveChatMessage(threadId: string, message: any): Promise<void>,
listChatMessages(threadId: string): Promise<{ messages: Array<any> }>,
applyWysiwygEdit(options: {