2
0

(api) Add textBubbleContentFormat option

Closes #1111
This commit is contained in:
Baptiste Arnaud
2024-05-23 16:30:56 +02:00
parent 3031d26f03
commit c53ce349af
22 changed files with 155 additions and 37 deletions

View File

@ -116,6 +116,7 @@ export const startWhatsAppPreview = authenticatedProcedure
startFrom, startFrom,
userId: user.id, userId: user.id,
isStreamEnabled: false, isStreamEnabled: false,
textBubbleContentFormat: 'richText',
}, },
initialSessionState: { initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined) whatsApp: (existingSession?.state as SessionState | undefined)

View File

@ -19,15 +19,24 @@ export const continueChat = publicProcedure
.describe( .describe(
'The session ID you got from the [startChat](./start-chat) response.' 'The session ID you got from the [startChat](./start-chat) response.'
), ),
textBubbleContentFormat: z
.enum(['richText', 'markdown'])
.default('richText'),
}) })
) )
.output(continueChatResponseSchema) .output(continueChatResponseSchema)
.mutation(async ({ input: { sessionId, message }, ctx: { origin, res } }) => { .mutation(
const { corsOrigin, ...response } = await continueChatFn({ async ({
origin, input: { sessionId, message, textBubbleContentFormat },
sessionId, ctx: { origin, res },
message, }) => {
}) const { corsOrigin, ...response } = await continueChatFn({
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin) origin,
return response sessionId,
}) message,
textBubbleContentFormat,
})
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
return response
}
)

View File

@ -92,6 +92,7 @@ export const sendMessageV1 = publicProcedure
: startParams.typebot, : startParams.typebot,
message, message,
userId: user?.id, userId: user?.id,
textBubbleContentFormat: 'richText',
} }
: { : {
type: 'live', type: 'live',
@ -101,6 +102,7 @@ export const sendMessageV1 = publicProcedure
prefilledVariables: startParams.prefilledVariables, prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId, resultId: startParams.resultId,
message, message,
textBubbleContentFormat: 'richText',
}, },
message, message,
}) })
@ -179,7 +181,11 @@ export const sendMessageV1 = publicProcedure
lastMessageNewFormat, lastMessageNewFormat,
visitedEdges, visitedEdges,
setVariableHistory, setVariableHistory,
} = await continueBotFlow(message, { version: 1, state: session.state }) } = await continueBotFlow(message, {
version: 1,
state: session.state,
textBubbleContentFormat: 'richText',
})
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs

View File

@ -92,6 +92,7 @@ export const sendMessageV2 = publicProcedure
: startParams.typebot, : startParams.typebot,
message, message,
userId: user?.id, userId: user?.id,
textBubbleContentFormat: 'richText',
} }
: { : {
type: 'live', type: 'live',
@ -101,6 +102,7 @@ export const sendMessageV2 = publicProcedure
prefilledVariables: startParams.prefilledVariables, prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId, resultId: startParams.resultId,
message, message,
textBubbleContentFormat: 'richText',
}, },
message, message,
}) })
@ -178,7 +180,11 @@ export const sendMessageV2 = publicProcedure
lastMessageNewFormat, lastMessageNewFormat,
visitedEdges, visitedEdges,
setVariableHistory, setVariableHistory,
} = await continueBotFlow(message, { version: 2, state: session.state }) } = await continueBotFlow(message, {
version: 2,
state: session.state,
textBubbleContentFormat: 'richText',
})
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs

View File

@ -28,6 +28,7 @@ export const startChatPreview = publicProcedure
typebot: startTypebot, typebot: startTypebot,
prefilledVariables, prefilledVariables,
sessionId, sessionId,
textBubbleContentFormat,
}, },
ctx: { user }, ctx: { user },
}) => }) =>
@ -41,5 +42,6 @@ export const startChatPreview = publicProcedure
userId: user?.id, userId: user?.id,
prefilledVariables, prefilledVariables,
sessionId, sessionId,
textBubbleContentFormat,
}) })
) )

View File

@ -48,6 +48,7 @@ test('API chat execution should work on preview bot', async ({ request }) => {
data: { data: {
isOnlyRegistering: false, isOnlyRegistering: false,
isStreamEnabled: false, isStreamEnabled: false,
textBubbleContentFormat: 'richText',
} satisfies Omit<StartPreviewChatInput, 'typebotId'>, } satisfies Omit<StartPreviewChatInput, 'typebotId'>,
}) })
).json() ).json()
@ -120,6 +121,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
data: { data: {
isOnlyRegistering: false, isOnlyRegistering: false,
isStreamEnabled: false, isStreamEnabled: false,
textBubbleContentFormat: 'richText',
} satisfies Omit<StartChatInput, 'publicId'>, } satisfies Omit<StartChatInput, 'publicId'>,
}) })
).json() ).json()
@ -302,6 +304,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
message: 'Hey', message: 'Hey',
isStreamEnabled: false, isStreamEnabled: false,
isOnlyRegistering: false, isOnlyRegistering: false,
textBubbleContentFormat: 'richText',
} satisfies Omit<StartChatInput, 'publicId'>, } satisfies Omit<StartChatInput, 'publicId'>,
} }
) )
@ -317,4 +320,19 @@ test('API chat execution should work on published bot', async ({ request }) => {
}, },
]) ])
}) })
await test.step('Markdown text bubble format should work', async () => {
const { messages } = await (
await request.post(`/api/v1/typebots/${typebotId}/preview/startChat`, {
data: {
isOnlyRegistering: false,
isStreamEnabled: false,
textBubbleContentFormat: 'markdown',
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
})
).json()
expect(messages[0].content.markdown).toStrictEqual('Hi there! 👋')
expect(messages[1].content.markdown).toStrictEqual(
'Welcome. What&#39;s your name?'
)
})
}) })

View File

@ -11,8 +11,14 @@ type Props = {
origin: string | undefined origin: string | undefined
message?: string message?: string
sessionId: string sessionId: string
textBubbleContentFormat: 'richText' | 'markdown'
} }
export const continueChat = async ({ origin, sessionId, message }: Props) => { export const continueChat = async ({
origin,
sessionId,
message,
textBubbleContentFormat,
}: Props) => {
const session = await getSession(sessionId) const session = await getSession(sessionId)
if (!session) { if (!session) {
@ -57,6 +63,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
version: 2, version: 2,
state: session.state, state: session.state,
startTime: Date.now(), startTime: Date.now(),
textBubbleContentFormat,
}) })
if (newSessionState) if (newSessionState)

View File

@ -12,6 +12,7 @@ type Props = {
isStreamEnabled: boolean isStreamEnabled: boolean
prefilledVariables?: Record<string, unknown> prefilledVariables?: Record<string, unknown>
resultId?: string resultId?: string
textBubbleContentFormat: 'richText' | 'markdown'
} }
export const startChat = async ({ export const startChat = async ({
@ -22,6 +23,7 @@ export const startChat = async ({
isStreamEnabled, isStreamEnabled,
prefilledVariables, prefilledVariables,
resultId: startResultId, resultId: startResultId,
textBubbleContentFormat,
}: Props) => { }: Props) => {
const { const {
typebot, typebot,
@ -43,6 +45,7 @@ export const startChat = async ({
publicId, publicId,
prefilledVariables, prefilledVariables,
resultId: startResultId, resultId: startResultId,
textBubbleContentFormat,
}, },
message, message,
}) })

View File

@ -14,6 +14,7 @@ type Props = {
userId?: string userId?: string
prefilledVariables?: Record<string, unknown> prefilledVariables?: Record<string, unknown>
sessionId?: string sessionId?: string
textBubbleContentFormat: 'richText' | 'markdown'
} }
export const startChatPreview = async ({ export const startChatPreview = async ({
@ -26,6 +27,7 @@ export const startChatPreview = async ({
userId, userId,
prefilledVariables, prefilledVariables,
sessionId, sessionId,
textBubbleContentFormat,
}: Props) => { }: Props) => {
const { const {
typebot, typebot,
@ -49,6 +51,7 @@ export const startChatPreview = async ({
userId, userId,
prefilledVariables, prefilledVariables,
sessionId, sessionId,
textBubbleContentFormat,
}, },
message, message,
}) })

View File

@ -50,10 +50,11 @@ type Params = {
version: 1 | 2 version: 1 | 2
state: SessionState state: SessionState
startTime?: number startTime?: number
textBubbleContentFormat: 'richText' | 'markdown'
} }
export const continueBotFlow = async ( export const continueBotFlow = async (
reply: Reply, reply: Reply,
{ state, version, startTime }: Params { state, version, startTime, textBubbleContentFormat }: Params
): Promise< ): Promise<
ContinueChatResponse & { ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
@ -66,7 +67,8 @@ export const continueBotFlow = async (
const visitedEdges: VisitedEdge[] = [] const visitedEdges: VisitedEdge[] = []
const setVariableHistory: SetVariableHistoryItem[] = [] const setVariableHistory: SetVariableHistoryItem[] = []
if (!newSessionState.currentBlockId) return startBotFlow({ state, version }) if (!newSessionState.currentBlockId)
return startBotFlow({ state, version, textBubbleContentFormat })
const { block, group, blockIndex } = getBlockById( const { block, group, blockIndex } = getBlockById(
newSessionState.currentBlockId, newSessionState.currentBlockId,
@ -167,7 +169,10 @@ export const continueBotFlow = async (
if (parsedReplyResult.status === 'fail') if (parsedReplyResult.status === 'fail')
return { return {
...(await parseRetryMessage(newSessionState)(block)), ...(await parseRetryMessage(newSessionState)(
block,
textBubbleContentFormat
)),
newSessionState, newSessionState,
visitedEdges: [], visitedEdges: [],
setVariableHistory: [], setVariableHistory: [],
@ -197,6 +202,7 @@ export const continueBotFlow = async (
setVariableHistory, setVariableHistory,
firstBubbleWasStreamed, firstBubbleWasStreamed,
startTime, startTime,
textBubbleContentFormat,
} }
) )
return { return {
@ -243,6 +249,7 @@ export const continueBotFlow = async (
visitedEdges, visitedEdges,
setVariableHistory, setVariableHistory,
startTime, startTime,
textBubbleContentFormat,
}) })
return { return {
@ -287,7 +294,8 @@ const saveVariableValueIfAny =
const parseRetryMessage = const parseRetryMessage =
(state: SessionState) => (state: SessionState) =>
async ( async (
block: InputBlock block: InputBlock,
textBubbleContentFormat: 'richText' | 'markdown'
): Promise<Pick<ContinueChatResponse, 'messages' | 'input'>> => { ): Promise<Pick<ContinueChatResponse, 'messages' | 'input'>> => {
const retryMessage = const retryMessage =
block.options && block.options &&
@ -302,9 +310,16 @@ const parseRetryMessage =
{ {
id: block.id, id: block.id,
type: BubbleBlockType.TEXT, type: BubbleBlockType.TEXT,
content: { content:
richText: [{ type: 'p', children: [{ text: retryMessage }] }], textBubbleContentFormat === 'richText'
}, ? {
type: 'richText',
richText: [{ type: 'p', children: [{ text: retryMessage }] }],
}
: {
type: 'markdown',
markdown: retryMessage,
},
}, },
], ],
input: await parseInput(state)(block), input: await parseInput(state)(block),

View File

@ -42,6 +42,7 @@ type ContextProps = {
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[] setVariableHistory: SetVariableHistoryItem[]
startTime?: number startTime?: number
textBubbleContentFormat: 'richText' | 'markdown'
} }
export const executeGroup = async ( export const executeGroup = async (
@ -55,6 +56,7 @@ export const executeGroup = async (
currentLastBubbleId, currentLastBubbleId,
firstBubbleWasStreamed, firstBubbleWasStreamed,
startTime, startTime,
textBubbleContentFormat,
}: ContextProps }: ContextProps
): Promise< ): Promise<
ContinueChatResponse & { ContinueChatResponse & {
@ -98,6 +100,7 @@ export const executeGroup = async (
version, version,
variables: newSessionState.typebotsQueue[0].typebot.variables, variables: newSessionState.typebotsQueue[0].typebot.variables,
typebotVersion: newSessionState.typebotsQueue[0].typebot.version, typebotVersion: newSessionState.typebotsQueue[0].typebot.version,
textBubbleContentFormat,
}) })
) )
lastBubbleBlockId = block.id lastBubbleBlockId = block.id
@ -250,6 +253,7 @@ export const executeGroup = async (
}, },
currentLastBubbleId: lastBubbleBlockId, currentLastBubbleId: lastBubbleBlockId,
startTime: newStartTime, startTime: newStartTime,
textBubbleContentFormat,
}) })
} }

View File

@ -11,15 +11,17 @@ import {
getVariablesToParseInfoInText, getVariablesToParseInfoInText,
parseVariables, parseVariables,
} from '@typebot.io/variables/parseVariables' } from '@typebot.io/variables/parseVariables'
import { TDescendant } from '@udecode/plate-common' import { TDescendant, TElement } from '@udecode/plate-common'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants' import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants' import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
import { convertMarkdownToRichText } from '@typebot.io/lib/markdown/convertMarkdownToRichText' import { convertMarkdownToRichText } from '@typebot.io/lib/markdown/convertMarkdownToRichText'
import { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
type Params = { type Params = {
version: 1 | 2 version: 1 | 2
typebotVersion: Typebot['version'] typebotVersion: Typebot['version']
variables: Variable[] variables: Variable[]
textBubbleContentFormat: 'richText' | 'markdown'
} }
export type BubbleBlockWithDefinedContent = BubbleBlock & { export type BubbleBlockWithDefinedContent = BubbleBlock & {
@ -28,7 +30,7 @@ export type BubbleBlockWithDefinedContent = BubbleBlock & {
export const parseBubbleBlock = ( export const parseBubbleBlock = (
block: BubbleBlockWithDefinedContent, block: BubbleBlockWithDefinedContent,
{ version, variables, typebotVersion }: Params { version, variables, typebotVersion, textBubbleContentFormat }: Params
): ContinueChatResponse['messages'][0] => { ): ContinueChatResponse['messages'][0] => {
switch (block.type) { switch (block.type) {
case BubbleBlockType.TEXT: { case BubbleBlockType.TEXT: {
@ -36,21 +38,29 @@ export const parseBubbleBlock = (
return { return {
...block, ...block,
content: { content: {
...block.content, type: 'richText',
richText: (block.content?.richText ?? []).map( richText: (block.content?.richText ?? []).map(
deepParseVariables(variables) deepParseVariables(variables)
), ),
}, },
} }
const richText = parseVariablesInRichText(block.content?.richText ?? [], {
variables,
takeLatestIfList: typebotVersion !== '6',
}).parsedElements
return { return {
...block, ...block,
content: { content:
...block.content, textBubbleContentFormat === 'richText'
richText: parseVariablesInRichText(block.content?.richText ?? [], { ? {
variables, type: 'richText',
takeLatestIfList: typebotVersion !== '6', richText,
}).parsedElements, }
}, : {
type: 'markdown',
markdown: convertRichTextToMarkdown(richText as TElement[]),
},
} }
} }

View File

@ -15,6 +15,7 @@ type Props = {
state: SessionState state: SessionState
startFrom?: StartFrom startFrom?: StartFrom
startTime?: number startTime?: number
textBubbleContentFormat: 'richText' | 'markdown'
} }
export const startBotFlow = async ({ export const startBotFlow = async ({
@ -22,6 +23,7 @@ export const startBotFlow = async ({
state, state,
startFrom, startFrom,
startTime, startTime,
textBubbleContentFormat,
}: Props): Promise< }: Props): Promise<
ContinueChatResponse & { ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
@ -47,6 +49,7 @@ export const startBotFlow = async ({
visitedEdges, visitedEdges,
setVariableHistory, setVariableHistory,
startTime, startTime,
textBubbleContentFormat,
}) })
} }
const firstEdgeId = getFirstEdgeId({ const firstEdgeId = getFirstEdgeId({
@ -74,5 +77,6 @@ export const startBotFlow = async ({
visitedEdges, visitedEdges,
setVariableHistory, setVariableHistory,
startTime, startTime,
textBubbleContentFormat,
}) })
} }

View File

@ -184,6 +184,7 @@ export const startSession = async ({
startFrom: startFrom:
startParams.type === 'preview' ? startParams.startFrom : undefined, startParams.type === 'preview' ? startParams.startFrom : undefined,
startTime: Date.now(), startTime: Date.now(),
textBubbleContentFormat: startParams.textBubbleContentFormat,
}) })
// If params has message and first block is an input block, we can directly continue the bot flow // If params has message and first block is an input block, we can directly continue the bot flow
@ -218,6 +219,7 @@ export const startSession = async ({
...newSessionState, ...newSessionState,
currentBlockId: firstBlock.id, currentBlockId: firstBlock.id,
}, },
textBubbleContentFormat: startParams.textBubbleContentFormat,
}) })
} }
} }

View File

@ -13,7 +13,8 @@ export const convertInputToWhatsAppMessages = (
lastMessage: ContinueChatResponse['messages'][number] | undefined lastMessage: ContinueChatResponse['messages'][number] | undefined
): WhatsAppSendingMessage[] => { ): WhatsAppSendingMessage[] => {
const lastMessageText = const lastMessageText =
lastMessage?.type === BubbleBlockType.TEXT lastMessage?.type === BubbleBlockType.TEXT &&
lastMessage.content.type === 'richText'
? convertRichTextToMarkdown(lastMessage.content.richText ?? [], { ? convertRichTextToMarkdown(lastMessage.content.richText ?? [], {
flavour: 'whatsapp', flavour: 'whatsapp',
}) })

View File

@ -17,6 +17,8 @@ export const convertMessageToWhatsAppMessage = (
): WhatsAppSendingMessage | null => { ): WhatsAppSendingMessage | null => {
switch (message.type) { switch (message.type) {
case BubbleBlockType.TEXT: { case BubbleBlockType.TEXT: {
if (message.content.type === 'markdown')
throw new Error('Expect rich text message')
if (!message.content.richText || message.content.richText.length === 0) if (!message.content.richText || message.content.richText.length === 0)
return null return null
return { return {

View File

@ -93,6 +93,7 @@ export const resumeWhatsAppFlow = async ({
? await continueBotFlow(reply, { ? await continueBotFlow(reply, {
version: 2, version: 2,
state: { ...session.state, whatsApp: { contact } }, state: { ...session.state, whatsApp: { contact } },
textBubbleContentFormat: 'richText',
}) })
: workspaceId : workspaceId
? await startWhatsAppSession({ ? await startWhatsAppSession({

View File

@ -58,7 +58,11 @@ export const sendChatReplyToWhatsApp = async ({
const result = await executeClientSideAction({ to, credentials })(action) const result = await executeClientSideAction({ to, credentials })(action)
if (!result) continue if (!result) continue
const { input, newSessionState, messages, clientSideActions } = const { input, newSessionState, messages, clientSideActions } =
await continueBotFlow(result.replyToSend, { version: 2, state }) await continueBotFlow(result.replyToSend, {
version: 2,
state,
textBubbleContentFormat: 'richText',
})
return sendChatReplyToWhatsApp({ return sendChatReplyToWhatsApp({
to, to,
@ -124,7 +128,11 @@ export const sendChatReplyToWhatsApp = async ({
) )
if (!result) continue if (!result) continue
const { input, newSessionState, messages, clientSideActions } = const { input, newSessionState, messages, clientSideActions } =
await continueBotFlow(result.replyToSend, { version: 2, state }) await continueBotFlow(result.replyToSend, {
version: 2,
state,
textBubbleContentFormat: 'richText',
})
return sendChatReplyToWhatsApp({ return sendChatReplyToWhatsApp({
to, to,

View File

@ -96,6 +96,7 @@ export const startWhatsAppSession = async ({
publicId: publicTypebot.typebot.publicId as string, publicId: publicTypebot.typebot.publicId as string,
isOnlyRegistering: false, isOnlyRegistering: false,
isStreamEnabled: false, isStreamEnabled: false,
textBubbleContentFormat: 'richText',
}, },
initialSessionState: { initialSessionState: {
whatsApp: { whatsApp: {

View File

@ -88,7 +88,7 @@ export async function startChatQuery({
sessionId, sessionId,
} satisfies Omit< } satisfies Omit<
StartPreviewChatInput, StartPreviewChatInput,
'typebotId' | 'isOnlyRegistering' 'typebotId' | 'isOnlyRegistering' | 'textBubbleContentFormat'
>, >,
timeout: false, timeout: false,
} }
@ -113,7 +113,10 @@ export async function startChatQuery({
prefilledVariables, prefilledVariables,
resultId, resultId,
isOnlyRegistering: false, isOnlyRegistering: false,
} satisfies Omit<StartChatInput, 'publicId'>, } satisfies Omit<
StartChatInput,
'publicId' | 'textBubbleContentFormat'
>,
timeout: false, timeout: false,
} }
) )

View File

@ -147,6 +147,7 @@ const executeGroup = ({
version: 2, version: 2,
variables: typebotsQueue[0].typebot.variables, variables: typebotsQueue[0].typebot.variables,
typebotVersion: typebotsQueue[0].typebot.version, typebotVersion: typebotsQueue[0].typebot.version,
textBubbleContentFormat: 'markdown',
} }
) )
const newMessage = const newMessage =
@ -321,11 +322,11 @@ const convertChatMessageToTranscriptMessage = (
): TranscriptMessage | null => { ): TranscriptMessage | null => {
switch (chatMessage.type) { switch (chatMessage.type) {
case BubbleBlockType.TEXT: { case BubbleBlockType.TEXT: {
if (!chatMessage.content.richText) return null if (chatMessage.content.type === 'richText') return null
return { return {
role: 'bot', role: 'bot',
type: 'text', type: 'text',
text: convertRichTextToMarkdown(chatMessage.content.richText), text: chatMessage.content.markdown,
} }
} }
case BubbleBlockType.IMAGE: { case BubbleBlockType.IMAGE: {

View File

@ -47,7 +47,16 @@ export type ChatSession = z.infer<typeof chatSessionSchema>
const textMessageSchema = z const textMessageSchema = z
.object({ .object({
type: z.literal(BubbleBlockType.TEXT), type: z.literal(BubbleBlockType.TEXT),
content: textBubbleContentSchema, content: z.discriminatedUnion('type', [
z.object({
type: z.literal('richText'),
richText: z.any(),
}),
z.object({
type: z.literal('markdown'),
markdown: z.string(),
}),
]),
}) })
.openapi({ .openapi({
title: 'Text', title: 'Text',
@ -211,6 +220,7 @@ export const startChatInputSchema = z.object({
Email: 'john@gmail.com', Email: 'john@gmail.com',
}, },
}), }),
textBubbleContentFormat: z.enum(['richText', 'markdown']).default('richText'),
}) })
export type StartChatInput = z.infer<typeof startChatInputSchema> export type StartChatInput = z.infer<typeof startChatInputSchema>
@ -265,6 +275,7 @@ export const startPreviewChatInputSchema = z.object({
.describe( .describe(
'If provided, will be used as the session ID and will overwrite any existing session with the same ID.' 'If provided, will be used as the session ID and will overwrite any existing session with the same ID.'
), ),
textBubbleContentFormat: z.enum(['richText', 'markdown']).default('richText'),
}) })
export type StartPreviewChatInput = z.infer<typeof startPreviewChatInputSchema> export type StartPreviewChatInput = z.infer<typeof startPreviewChatInputSchema>