diff --git a/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts b/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts index 02017c662..dc4f034cd 100644 --- a/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts +++ b/apps/backend/src/app/api/latest/ai/query/[mode]/route.ts @@ -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", ""); diff --git a/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx b/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx index 1204da949..0b409c0b9 100644 --- a/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx +++ b/apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx @@ -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 = []; - 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: { diff --git a/apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx b/apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx index adb4a4017..697ab00b8 100644 --- a/apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx +++ b/apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx @@ -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(); diff --git a/apps/backend/src/lib/ai-chat/adapter-registry.ts b/apps/backend/src/lib/ai-chat/adapter-registry.ts deleted file mode 100644 index 63106a2ce..000000000 --- a/apps/backend/src/lib/ai-chat/adapter-registry.ts +++ /dev/null @@ -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, -} - -type ContextType = "email-theme" | "email-template" | "email-draft"; - -const CHAT_ADAPTERS: Record 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 }); -} diff --git a/apps/backend/src/lib/ai-chat/email-draft-adapter.ts b/apps/backend/src/lib/ai-chat/email-draft-adapter.ts deleted file mode 100644 index c5c5687e0..000000000 --- a/apps/backend/src/lib/ai-chat/email-draft-adapter.ts +++ /dev/null @@ -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 , , , or 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 . -- Do NOT include , , , or 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 ( - - - -
Hi {user.displayName}!
-
-
- ); -} -\`\`\` -`; -}; diff --git a/apps/backend/src/lib/ai-chat/email-template-adapter.ts b/apps/backend/src/lib/ai-chat/email-template-adapter.ts deleted file mode 100644 index 299d7cf43..000000000 --- a/apps/backend/src/lib/ai-chat/email-template-adapter.ts +++ /dev/null @@ -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 , , , or 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 component. -- Always include a component. -- Do NOT include , , , or 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) { - return ( - - - -
Hi {user.displayName}!
-
- count is {variables.count} -
- ); -} - -EmailTemplate.PreviewVariables = { - count: 10 -} satisfies typeof variablesSchema.infer -\`\`\` - -Here is the user's current email template: -\`\`\`tsx -${currentEmailTemplate.tsxSource} -\`\`\` -`; -}; diff --git a/apps/backend/src/lib/ai-chat/email-theme-adapter.ts b/apps/backend/src/lib/ai-chat/email-theme-adapter.ts deleted file mode 100644 index d9fbe85f9..000000000 --- a/apps/backend/src/lib/ai-chat/email-theme-adapter.ts +++ /dev/null @@ -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 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 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 ( - - - - {children} - - - ) -} -\`\`\` - -Here is the current email theme: -\`\`\`tsx -${currentEmailTheme} -\`\`\` -`; -}; diff --git a/apps/backend/src/lib/ai/prompts.ts b/apps/backend/src/lib/ai/prompts.ts index 1658af4c5..c88d93677 100644 --- a/apps/backend/src/lib/ai/prompts.ts +++ b/apps/backend/src/lib/ai/prompts.ts @@ -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 , , , or 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 component -- Always include a component -- Do NOT include , , , or 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 , , , or — the email theme provides those wrappers. +2. Always include a component with a meaningful value. +3. Always include a 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\` 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 \`
\`, import \`Hr\`. If you use \`\`, 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 \`\`, not \`<Container>\`. +13. NEVER use bare & in JSX text content — it is invalid JSX and causes a build error. Use \`&\` 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 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 \`\` 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 , , and a 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 \`
\`, 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 \`\`, not \`<EmailTheme>\`. +10. NEVER use bare & in JSX text content — it is invalid JSX and causes a build error. Use \`&\` or \`{"&"}\` instead. +11. Do NOT pass a \`config\` prop to \`\`. 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 , , , or 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 component -- Do NOT include , , , or 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 . +- Do NOT include , , , or 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": ` diff --git a/apps/backend/src/lib/ai/tools/create-email-draft.ts b/apps/backend/src/lib/ai/tools/create-email-draft.ts index e494747f7..4af99f8f2 100644 --- a/apps/backend/src/lib/ai/tools/create-email-draft.ts +++ b/apps/backend/src/lib/ai/tools/create-email-draft.ts @@ -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 , , , , , and 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 component must be rendered inside 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 ( - + - - - Welcome, {user.displayName}! - - - - Thank you for joining {project.displayName}. We're excited to have you here. - - - - Get started by visiting your dashboard and exploring the features. - - - +
Hi {user.displayName}!
+
); } \`\`\` -**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 }); diff --git a/apps/backend/src/lib/ai/tools/create-email-template.ts b/apps/backend/src/lib/ai/tools/create-email-template.ts index 1b9436e95..a35d0ceae 100644 --- a/apps/backend/src/lib/ai/tools/create-email-template.ts +++ b/apps/backend/src/lib/ai/tools/create-email-template.ts @@ -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 , , , , , and 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 component must be rendered inside 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 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) { return ( - + - - - Hi {user.displayName}! - - - - Please complete your action within {variables.expiresInHours} hours. - - - +
Hi {user.displayName}!
+
+ count is {variables.count}
); } 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 }); diff --git a/apps/backend/src/lib/ai/tools/create-email-theme.ts b/apps/backend/src/lib/ai/tools/create-email-theme.ts index 7ec2f3b3c..5b88b0a6f 100644 --- a/apps/backend/src/lib/ai/tools/create-email-theme.ts +++ b/apps/backend/src/lib/ai/tools/create-email-theme.ts @@ -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 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 \`\`, \`\`, and a \`\` 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 \`\` +- 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 ( - - {children} - + + +
+ {children} +
+
+
+ {unsubscribeLink && ( + + Unsubscribe + + )} +
+
+
) } \`\`\` - -**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 }); diff --git a/apps/dashboard/.env.development b/apps/dashboard/.env.development index 8a4c35046..5e62be0aa 100644 --- a/apps/dashboard/.env.development +++ b/apps/dashboard/.env.development @@ -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 diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index d268f512f..4d805f13f 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -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", diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx index 82b144063..c70ce90ac 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx @@ -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={ currentCode)} toolComponents={} useOffWhiteLightMode /> diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/[templateId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/[templateId]/page-client.tsx index e792ae8d3..d0a61daea 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/[templateId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/[templateId]/page-client.tsx @@ -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={ currentCode)} historyAdapter={createHistoryAdapter(stackAdminApp, template.id)} toolComponents={} useOffWhiteLightMode diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/[themeId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/[themeId]/page-client.tsx index f7faee0c6..8770ccf37 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/[themeId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/[themeId]/page-client.tsx @@ -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={ currentCode)} historyAdapter={createHistoryAdapter(stackAdminApp, themeId)} toolComponents={} useOffWhiteLightMode diff --git a/apps/dashboard/src/app/api/ai-search/route.ts b/apps/dashboard/src/app/api/ai-search/route.ts index 9e4add07a..1e8216504 100644 --- a/apps/dashboard/src/app/api/ai-search/route.ts +++ b/apps/dashboard/src/app/api/ai-search/route.ts @@ -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; diff --git a/apps/dashboard/src/app/api/email-ai/route.ts b/apps/dashboard/src/app/api/email-ai/route.ts new file mode 100644 index 000000000..05c3a4a5a --- /dev/null +++ b/apps/dashboard/src/app/api/email-ai/route.ts @@ -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 (<Component> instead of ) + * 3. Bare & in JSX text content (invalid JSX; must be & 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 `&&` (should be `&&`) and `<Container>`. + // Only decodes the entities we expect in generated TSX. + result = result + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/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" }, + }); +} diff --git a/apps/dashboard/src/components/vibe-coding/chat-adapters.ts b/apps/dashboard/src/components/vibe-coding/chat-adapters.ts index a2480d05c..918a2a015 100644 --- a/apps/dashboard/src/components/vibe-coding/chat-adapters.ts +++ b/apps/dashboard/src/components/vibe-coding/chat-adapters.ts @@ -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 {}; diff --git a/apps/dashboard/src/components/vibe-coding/draft-tool-components.tsx b/apps/dashboard/src/components/vibe-coding/draft-tool-components.tsx index 3f0a0f87c..8a69bbbe5 100644 --- a/apps/dashboard/src/components/vibe-coding/draft-tool-components.tsx +++ b/apps/dashboard/src/components/vibe-coding/draft-tool-components.tsx @@ -11,7 +11,7 @@ export const EmailDraftUI = ({ setCurrentCode }: EmailDraftUIProps) => { { content: string }, "success" >({ - toolName: "createEmailTemplate", + toolName: "createEmailDraft", render: ({ args }) => { return ( diff --git a/packages/stack-shared/src/interface/admin-interface.ts b/packages/stack-shared/src/interface/admin-interface.ts index 0c10351d3..9cd597cee 100644 --- a/packages/stack-shared/src/interface/admin-interface.ts +++ b/packages/stack-shared/src/interface/admin-interface.ts @@ -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 { await this.sendAdminRequest( `/internal/ai-chat/${threadId}`, diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index 389f53db7..73bb2774a 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -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, - abortSignal?: AbortSignal, - ): Promise<{ content: ChatContent }> { - return await this._interface.sendChatMessage(threadId, contextType, messages, abortSignal); - } - async saveChatMessage(threadId: string, message: any): Promise { await this._interface.saveChatMessage(threadId, message); } diff --git a/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts b/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts index 5e3c6db5f..4864b4351 100644 --- a/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts +++ b/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts @@ -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, updateEmailTheme(id: string, tsxSource: string): Promise, deleteEmailTheme(id: string): Promise, - - 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, listChatMessages(threadId: string): Promise<{ messages: Array }>, applyWysiwygEdit(options: {