@ -116,6 +116,7 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
startFrom,
|
||||
userId: user.id,
|
||||
isStreamEnabled: false,
|
||||
textBubbleContentFormat: 'richText',
|
||||
},
|
||||
initialSessionState: {
|
||||
whatsApp: (existingSession?.state as SessionState | undefined)
|
||||
|
@ -19,15 +19,24 @@ export const continueChat = publicProcedure
|
||||
.describe(
|
||||
'The session ID you got from the [startChat](./start-chat) response.'
|
||||
),
|
||||
textBubbleContentFormat: z
|
||||
.enum(['richText', 'markdown'])
|
||||
.default('richText'),
|
||||
})
|
||||
)
|
||||
.output(continueChatResponseSchema)
|
||||
.mutation(async ({ input: { sessionId, message }, ctx: { origin, res } }) => {
|
||||
.mutation(
|
||||
async ({
|
||||
input: { sessionId, message, textBubbleContentFormat },
|
||||
ctx: { origin, res },
|
||||
}) => {
|
||||
const { corsOrigin, ...response } = await continueChatFn({
|
||||
origin,
|
||||
sessionId,
|
||||
message,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
|
||||
return response
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -92,6 +92,7 @@ export const sendMessageV1 = publicProcedure
|
||||
: startParams.typebot,
|
||||
message,
|
||||
userId: user?.id,
|
||||
textBubbleContentFormat: 'richText',
|
||||
}
|
||||
: {
|
||||
type: 'live',
|
||||
@ -101,6 +102,7 @@ export const sendMessageV1 = publicProcedure
|
||||
prefilledVariables: startParams.prefilledVariables,
|
||||
resultId: startParams.resultId,
|
||||
message,
|
||||
textBubbleContentFormat: 'richText',
|
||||
},
|
||||
message,
|
||||
})
|
||||
@ -179,7 +181,11 @@ export const sendMessageV1 = publicProcedure
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
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
|
||||
|
||||
|
@ -92,6 +92,7 @@ export const sendMessageV2 = publicProcedure
|
||||
: startParams.typebot,
|
||||
message,
|
||||
userId: user?.id,
|
||||
textBubbleContentFormat: 'richText',
|
||||
}
|
||||
: {
|
||||
type: 'live',
|
||||
@ -101,6 +102,7 @@ export const sendMessageV2 = publicProcedure
|
||||
prefilledVariables: startParams.prefilledVariables,
|
||||
resultId: startParams.resultId,
|
||||
message,
|
||||
textBubbleContentFormat: 'richText',
|
||||
},
|
||||
message,
|
||||
})
|
||||
@ -178,7 +180,11 @@ export const sendMessageV2 = publicProcedure
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
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
|
||||
|
||||
|
@ -28,6 +28,7 @@ export const startChatPreview = publicProcedure
|
||||
typebot: startTypebot,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
textBubbleContentFormat,
|
||||
},
|
||||
ctx: { user },
|
||||
}) =>
|
||||
@ -41,5 +42,6 @@ export const startChatPreview = publicProcedure
|
||||
userId: user?.id,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
)
|
||||
|
@ -48,6 +48,7 @@ test('API chat execution should work on preview bot', async ({ request }) => {
|
||||
data: {
|
||||
isOnlyRegistering: false,
|
||||
isStreamEnabled: false,
|
||||
textBubbleContentFormat: 'richText',
|
||||
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
|
||||
})
|
||||
).json()
|
||||
@ -120,6 +121,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
data: {
|
||||
isOnlyRegistering: false,
|
||||
isStreamEnabled: false,
|
||||
textBubbleContentFormat: 'richText',
|
||||
} satisfies Omit<StartChatInput, 'publicId'>,
|
||||
})
|
||||
).json()
|
||||
@ -302,6 +304,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
message: 'Hey',
|
||||
isStreamEnabled: false,
|
||||
isOnlyRegistering: false,
|
||||
textBubbleContentFormat: 'richText',
|
||||
} 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's your name?'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -11,8 +11,14 @@ type Props = {
|
||||
origin: string | undefined
|
||||
message?: 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)
|
||||
|
||||
if (!session) {
|
||||
@ -57,6 +63,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
|
||||
version: 2,
|
||||
state: session.state,
|
||||
startTime: Date.now(),
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
|
||||
if (newSessionState)
|
||||
|
@ -12,6 +12,7 @@ type Props = {
|
||||
isStreamEnabled: boolean
|
||||
prefilledVariables?: Record<string, unknown>
|
||||
resultId?: string
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
}
|
||||
|
||||
export const startChat = async ({
|
||||
@ -22,6 +23,7 @@ export const startChat = async ({
|
||||
isStreamEnabled,
|
||||
prefilledVariables,
|
||||
resultId: startResultId,
|
||||
textBubbleContentFormat,
|
||||
}: Props) => {
|
||||
const {
|
||||
typebot,
|
||||
@ -43,6 +45,7 @@ export const startChat = async ({
|
||||
publicId,
|
||||
prefilledVariables,
|
||||
resultId: startResultId,
|
||||
textBubbleContentFormat,
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
@ -14,6 +14,7 @@ type Props = {
|
||||
userId?: string
|
||||
prefilledVariables?: Record<string, unknown>
|
||||
sessionId?: string
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
}
|
||||
|
||||
export const startChatPreview = async ({
|
||||
@ -26,6 +27,7 @@ export const startChatPreview = async ({
|
||||
userId,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
textBubbleContentFormat,
|
||||
}: Props) => {
|
||||
const {
|
||||
typebot,
|
||||
@ -49,6 +51,7 @@ export const startChatPreview = async ({
|
||||
userId,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
textBubbleContentFormat,
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
@ -50,10 +50,11 @@ type Params = {
|
||||
version: 1 | 2
|
||||
state: SessionState
|
||||
startTime?: number
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
}
|
||||
export const continueBotFlow = async (
|
||||
reply: Reply,
|
||||
{ state, version, startTime }: Params
|
||||
{ state, version, startTime, textBubbleContentFormat }: Params
|
||||
): Promise<
|
||||
ContinueChatResponse & {
|
||||
newSessionState: SessionState
|
||||
@ -66,7 +67,8 @@ export const continueBotFlow = async (
|
||||
const visitedEdges: VisitedEdge[] = []
|
||||
const setVariableHistory: SetVariableHistoryItem[] = []
|
||||
|
||||
if (!newSessionState.currentBlockId) return startBotFlow({ state, version })
|
||||
if (!newSessionState.currentBlockId)
|
||||
return startBotFlow({ state, version, textBubbleContentFormat })
|
||||
|
||||
const { block, group, blockIndex } = getBlockById(
|
||||
newSessionState.currentBlockId,
|
||||
@ -167,7 +169,10 @@ export const continueBotFlow = async (
|
||||
|
||||
if (parsedReplyResult.status === 'fail')
|
||||
return {
|
||||
...(await parseRetryMessage(newSessionState)(block)),
|
||||
...(await parseRetryMessage(newSessionState)(
|
||||
block,
|
||||
textBubbleContentFormat
|
||||
)),
|
||||
newSessionState,
|
||||
visitedEdges: [],
|
||||
setVariableHistory: [],
|
||||
@ -197,6 +202,7 @@ export const continueBotFlow = async (
|
||||
setVariableHistory,
|
||||
firstBubbleWasStreamed,
|
||||
startTime,
|
||||
textBubbleContentFormat,
|
||||
}
|
||||
)
|
||||
return {
|
||||
@ -243,6 +249,7 @@ export const continueBotFlow = async (
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
startTime,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -287,7 +294,8 @@ const saveVariableValueIfAny =
|
||||
const parseRetryMessage =
|
||||
(state: SessionState) =>
|
||||
async (
|
||||
block: InputBlock
|
||||
block: InputBlock,
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
): Promise<Pick<ContinueChatResponse, 'messages' | 'input'>> => {
|
||||
const retryMessage =
|
||||
block.options &&
|
||||
@ -302,8 +310,15 @@ const parseRetryMessage =
|
||||
{
|
||||
id: block.id,
|
||||
type: BubbleBlockType.TEXT,
|
||||
content: {
|
||||
content:
|
||||
textBubbleContentFormat === 'richText'
|
||||
? {
|
||||
type: 'richText',
|
||||
richText: [{ type: 'p', children: [{ text: retryMessage }] }],
|
||||
}
|
||||
: {
|
||||
type: 'markdown',
|
||||
markdown: retryMessage,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -42,6 +42,7 @@ type ContextProps = {
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
startTime?: number
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
}
|
||||
|
||||
export const executeGroup = async (
|
||||
@ -55,6 +56,7 @@ export const executeGroup = async (
|
||||
currentLastBubbleId,
|
||||
firstBubbleWasStreamed,
|
||||
startTime,
|
||||
textBubbleContentFormat,
|
||||
}: ContextProps
|
||||
): Promise<
|
||||
ContinueChatResponse & {
|
||||
@ -98,6 +100,7 @@ export const executeGroup = async (
|
||||
version,
|
||||
variables: newSessionState.typebotsQueue[0].typebot.variables,
|
||||
typebotVersion: newSessionState.typebotsQueue[0].typebot.version,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
)
|
||||
lastBubbleBlockId = block.id
|
||||
@ -250,6 +253,7 @@ export const executeGroup = async (
|
||||
},
|
||||
currentLastBubbleId: lastBubbleBlockId,
|
||||
startTime: newStartTime,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -11,15 +11,17 @@ import {
|
||||
getVariablesToParseInfoInText,
|
||||
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 { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||
import { convertMarkdownToRichText } from '@typebot.io/lib/markdown/convertMarkdownToRichText'
|
||||
import { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
|
||||
|
||||
type Params = {
|
||||
version: 1 | 2
|
||||
typebotVersion: Typebot['version']
|
||||
variables: Variable[]
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
}
|
||||
|
||||
export type BubbleBlockWithDefinedContent = BubbleBlock & {
|
||||
@ -28,7 +30,7 @@ export type BubbleBlockWithDefinedContent = BubbleBlock & {
|
||||
|
||||
export const parseBubbleBlock = (
|
||||
block: BubbleBlockWithDefinedContent,
|
||||
{ version, variables, typebotVersion }: Params
|
||||
{ version, variables, typebotVersion, textBubbleContentFormat }: Params
|
||||
): ContinueChatResponse['messages'][0] => {
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
@ -36,20 +38,28 @@ export const parseBubbleBlock = (
|
||||
return {
|
||||
...block,
|
||||
content: {
|
||||
...block.content,
|
||||
type: 'richText',
|
||||
richText: (block.content?.richText ?? []).map(
|
||||
deepParseVariables(variables)
|
||||
),
|
||||
},
|
||||
}
|
||||
return {
|
||||
...block,
|
||||
content: {
|
||||
...block.content,
|
||||
richText: parseVariablesInRichText(block.content?.richText ?? [], {
|
||||
|
||||
const richText = parseVariablesInRichText(block.content?.richText ?? [], {
|
||||
variables,
|
||||
takeLatestIfList: typebotVersion !== '6',
|
||||
}).parsedElements,
|
||||
}).parsedElements
|
||||
return {
|
||||
...block,
|
||||
content:
|
||||
textBubbleContentFormat === 'richText'
|
||||
? {
|
||||
type: 'richText',
|
||||
richText,
|
||||
}
|
||||
: {
|
||||
type: 'markdown',
|
||||
markdown: convertRichTextToMarkdown(richText as TElement[]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ type Props = {
|
||||
state: SessionState
|
||||
startFrom?: StartFrom
|
||||
startTime?: number
|
||||
textBubbleContentFormat: 'richText' | 'markdown'
|
||||
}
|
||||
|
||||
export const startBotFlow = async ({
|
||||
@ -22,6 +23,7 @@ export const startBotFlow = async ({
|
||||
state,
|
||||
startFrom,
|
||||
startTime,
|
||||
textBubbleContentFormat,
|
||||
}: Props): Promise<
|
||||
ContinueChatResponse & {
|
||||
newSessionState: SessionState
|
||||
@ -47,6 +49,7 @@ export const startBotFlow = async ({
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
startTime,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
}
|
||||
const firstEdgeId = getFirstEdgeId({
|
||||
@ -74,5 +77,6 @@ export const startBotFlow = async ({
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
startTime,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
}
|
||||
|
@ -184,6 +184,7 @@ export const startSession = async ({
|
||||
startFrom:
|
||||
startParams.type === 'preview' ? startParams.startFrom : undefined,
|
||||
startTime: Date.now(),
|
||||
textBubbleContentFormat: startParams.textBubbleContentFormat,
|
||||
})
|
||||
|
||||
// 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,
|
||||
currentBlockId: firstBlock.id,
|
||||
},
|
||||
textBubbleContentFormat: startParams.textBubbleContentFormat,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ export const convertInputToWhatsAppMessages = (
|
||||
lastMessage: ContinueChatResponse['messages'][number] | undefined
|
||||
): WhatsAppSendingMessage[] => {
|
||||
const lastMessageText =
|
||||
lastMessage?.type === BubbleBlockType.TEXT
|
||||
lastMessage?.type === BubbleBlockType.TEXT &&
|
||||
lastMessage.content.type === 'richText'
|
||||
? convertRichTextToMarkdown(lastMessage.content.richText ?? [], {
|
||||
flavour: 'whatsapp',
|
||||
})
|
||||
|
@ -17,6 +17,8 @@ export const convertMessageToWhatsAppMessage = (
|
||||
): WhatsAppSendingMessage | null => {
|
||||
switch (message.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
if (message.content.type === 'markdown')
|
||||
throw new Error('Expect rich text message')
|
||||
if (!message.content.richText || message.content.richText.length === 0)
|
||||
return null
|
||||
return {
|
||||
|
@ -93,6 +93,7 @@ export const resumeWhatsAppFlow = async ({
|
||||
? await continueBotFlow(reply, {
|
||||
version: 2,
|
||||
state: { ...session.state, whatsApp: { contact } },
|
||||
textBubbleContentFormat: 'richText',
|
||||
})
|
||||
: workspaceId
|
||||
? await startWhatsAppSession({
|
||||
|
@ -58,7 +58,11 @@ export const sendChatReplyToWhatsApp = async ({
|
||||
const result = await executeClientSideAction({ to, credentials })(action)
|
||||
if (!result) continue
|
||||
const { input, newSessionState, messages, clientSideActions } =
|
||||
await continueBotFlow(result.replyToSend, { version: 2, state })
|
||||
await continueBotFlow(result.replyToSend, {
|
||||
version: 2,
|
||||
state,
|
||||
textBubbleContentFormat: 'richText',
|
||||
})
|
||||
|
||||
return sendChatReplyToWhatsApp({
|
||||
to,
|
||||
@ -124,7 +128,11 @@ export const sendChatReplyToWhatsApp = async ({
|
||||
)
|
||||
if (!result) continue
|
||||
const { input, newSessionState, messages, clientSideActions } =
|
||||
await continueBotFlow(result.replyToSend, { version: 2, state })
|
||||
await continueBotFlow(result.replyToSend, {
|
||||
version: 2,
|
||||
state,
|
||||
textBubbleContentFormat: 'richText',
|
||||
})
|
||||
|
||||
return sendChatReplyToWhatsApp({
|
||||
to,
|
||||
|
@ -96,6 +96,7 @@ export const startWhatsAppSession = async ({
|
||||
publicId: publicTypebot.typebot.publicId as string,
|
||||
isOnlyRegistering: false,
|
||||
isStreamEnabled: false,
|
||||
textBubbleContentFormat: 'richText',
|
||||
},
|
||||
initialSessionState: {
|
||||
whatsApp: {
|
||||
|
@ -88,7 +88,7 @@ export async function startChatQuery({
|
||||
sessionId,
|
||||
} satisfies Omit<
|
||||
StartPreviewChatInput,
|
||||
'typebotId' | 'isOnlyRegistering'
|
||||
'typebotId' | 'isOnlyRegistering' | 'textBubbleContentFormat'
|
||||
>,
|
||||
timeout: false,
|
||||
}
|
||||
@ -113,7 +113,10 @@ export async function startChatQuery({
|
||||
prefilledVariables,
|
||||
resultId,
|
||||
isOnlyRegistering: false,
|
||||
} satisfies Omit<StartChatInput, 'publicId'>,
|
||||
} satisfies Omit<
|
||||
StartChatInput,
|
||||
'publicId' | 'textBubbleContentFormat'
|
||||
>,
|
||||
timeout: false,
|
||||
}
|
||||
)
|
||||
|
@ -147,6 +147,7 @@ const executeGroup = ({
|
||||
version: 2,
|
||||
variables: typebotsQueue[0].typebot.variables,
|
||||
typebotVersion: typebotsQueue[0].typebot.version,
|
||||
textBubbleContentFormat: 'markdown',
|
||||
}
|
||||
)
|
||||
const newMessage =
|
||||
@ -321,11 +322,11 @@ const convertChatMessageToTranscriptMessage = (
|
||||
): TranscriptMessage | null => {
|
||||
switch (chatMessage.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
if (!chatMessage.content.richText) return null
|
||||
if (chatMessage.content.type === 'richText') return null
|
||||
return {
|
||||
role: 'bot',
|
||||
type: 'text',
|
||||
text: convertRichTextToMarkdown(chatMessage.content.richText),
|
||||
text: chatMessage.content.markdown,
|
||||
}
|
||||
}
|
||||
case BubbleBlockType.IMAGE: {
|
||||
|
@ -47,7 +47,16 @@ export type ChatSession = z.infer<typeof chatSessionSchema>
|
||||
const textMessageSchema = z
|
||||
.object({
|
||||
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({
|
||||
title: 'Text',
|
||||
@ -211,6 +220,7 @@ export const startChatInputSchema = z.object({
|
||||
Email: 'john@gmail.com',
|
||||
},
|
||||
}),
|
||||
textBubbleContentFormat: z.enum(['richText', 'markdown']).default('richText'),
|
||||
})
|
||||
export type StartChatInput = z.infer<typeof startChatInputSchema>
|
||||
|
||||
@ -265,6 +275,7 @@ export const startPreviewChatInputSchema = z.object({
|
||||
.describe(
|
||||
'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>
|
||||
|
||||
|
Reference in New Issue
Block a user