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,
userId: user.id,
isStreamEnabled: false,
textBubbleContentFormat: 'richText',
},
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)

View File

@ -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 } }) => {
const { corsOrigin, ...response } = await continueChatFn({
origin,
sessionId,
message,
})
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
return response
})
.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
}
)

View File

@ -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

View File

@ -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

View File

@ -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,
})
)

View File

@ -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&#39;s your name?'
)
})
})

View File

@ -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)

View File

@ -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,
})

View File

@ -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,
})

View File

@ -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,9 +310,16 @@ const parseRetryMessage =
{
id: block.id,
type: BubbleBlockType.TEXT,
content: {
richText: [{ type: 'p', children: [{ text: retryMessage }] }],
},
content:
textBubbleContentFormat === 'richText'
? {
type: 'richText',
richText: [{ type: 'p', children: [{ text: retryMessage }] }],
}
: {
type: 'markdown',
markdown: retryMessage,
},
},
],
input: await parseInput(state)(block),

View File

@ -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,
})
}

View File

@ -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,21 +38,29 @@ export const parseBubbleBlock = (
return {
...block,
content: {
...block.content,
type: 'richText',
richText: (block.content?.richText ?? []).map(
deepParseVariables(variables)
),
},
}
const richText = parseVariablesInRichText(block.content?.richText ?? [], {
variables,
takeLatestIfList: typebotVersion !== '6',
}).parsedElements
return {
...block,
content: {
...block.content,
richText: parseVariablesInRichText(block.content?.richText ?? [], {
variables,
takeLatestIfList: typebotVersion !== '6',
}).parsedElements,
},
content:
textBubbleContentFormat === 'richText'
? {
type: 'richText',
richText,
}
: {
type: 'markdown',
markdown: convertRichTextToMarkdown(richText as TElement[]),
},
}
}

View File

@ -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,
})
}

View File

@ -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,
})
}
}

View File

@ -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',
})

View File

@ -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 {

View File

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

View File

@ -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,

View File

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

View File

@ -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,
}
)

View File

@ -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: {

View File

@ -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>