From af1bee8756015557f1aec44d11a9f9b1fc8fed6e Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 7 Aug 2023 15:22:05 +0200 Subject: [PATCH] :children_crossing: (bot) Update reply if we get new format from backend --- apps/docs/openapi/builder/_spec_.json | 4 ++ apps/docs/openapi/chat/_spec_.json | 11 ++++++ .../src/features/chat/api/sendMessage.ts | 11 +++++- .../features/chat/helpers/continueBotFlow.ts | 39 ++++++++++++++----- packages/embeds/js/package.json | 2 +- .../ConversationContainer/ChatChunk.tsx | 4 +- .../ConversationContainer.tsx | 13 +++++++ .../js/src/components/InputChatBlock.tsx | 13 ++++++- .../js/src/utils/formattedMessagesSignal.ts | 5 +++ packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- packages/schemas/features/chat.ts | 6 +++ 12 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 packages/embeds/js/src/utils/formattedMessagesSignal.ts diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index 40963394c..cc3c24186 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -744,6 +744,9 @@ "properties": { "url": { "type": "string" + }, + "isAutoplayEnabled": { + "type": "boolean" } }, "additionalProperties": false @@ -1986,6 +1989,7 @@ "Custom", "Empty", "User ID", + "Now", "Today", "Yesterday", "Tomorrow", diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 996529bb2..e78949b8f 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -312,6 +312,9 @@ "properties": { "url": { "type": "string" + }, + "isAutoplayEnabled": { + "type": "boolean" } }, "additionalProperties": false @@ -1554,6 +1557,7 @@ "Custom", "Empty", "User ID", + "Now", "Today", "Yesterday", "Tomorrow", @@ -4031,6 +4035,9 @@ "properties": { "url": { "type": "string" + }, + "isAutoplayEnabled": { + "type": "boolean" } }, "additionalProperties": false @@ -6021,6 +6028,10 @@ ], "additionalProperties": false } + }, + "lastMessageNewFormat": { + "type": "string", + "description": "The sent message is validated and formatted on the backend. This is set only if the message differs from the formatted version." } }, "required": [ diff --git a/apps/viewer/src/features/chat/api/sendMessage.ts b/apps/viewer/src/features/chat/api/sendMessage.ts index a115a2a21..462a43b5f 100644 --- a/apps/viewer/src/features/chat/api/sendMessage.ts +++ b/apps/viewer/src/features/chat/api/sendMessage.ts @@ -79,8 +79,14 @@ export const sendMessage = publicProcedure clientSideActions, } } else { - const { messages, input, clientSideActions, newSessionState, logs } = - await continueBotFlow(session.state)(message) + const { + messages, + input, + clientSideActions, + newSessionState, + logs, + lastMessageNewFormat, + } = await continueBotFlow(session.state)(message) const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs @@ -101,6 +107,7 @@ export const sendMessage = publicProcedure clientSideActions, dynamicTheme: parseDynamicThemeReply(newSessionState), logs, + lastMessageNewFormat, } } } diff --git a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts b/apps/viewer/src/features/chat/helpers/continueBotFlow.ts index 1814dbd20..1ccae6099 100644 --- a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts +++ b/apps/viewer/src/features/chat/helpers/continueBotFlow.ts @@ -85,7 +85,7 @@ export const continueBotFlow = message: 'Current block is not an input block', }) - let formattedReply = null + let formattedReply: string | undefined if (isInputBlock(block)) { if (reply && !isReplyValid(reply, block)) @@ -116,25 +116,46 @@ export const continueBotFlow = const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply) if (groupHasMoreBlocks && !nextEdgeId) { - return executeGroup(newSessionState)({ + const chatReply = await executeGroup(newSessionState)({ ...group, blocks: group.blocks.slice(blockIndex + 1), }) + return { + ...chatReply, + lastMessageNewFormat: + formattedReply !== reply ? formattedReply : undefined, + } } if (!nextEdgeId && state.linkedTypebots.queue.length === 0) - return { messages: [], newSessionState } + return { + messages: [], + newSessionState, + lastMessageNewFormat: + formattedReply !== reply ? formattedReply : undefined, + } const nextGroup = getNextGroup(newSessionState)(nextEdgeId) - if (!nextGroup) return { messages: [], newSessionState } + if (!nextGroup) + return { + messages: [], + newSessionState, + lastMessageNewFormat: + formattedReply !== reply ? formattedReply : undefined, + } - return executeGroup(newSessionState)(nextGroup.group) + const chatReply = executeGroup(newSessionState)(nextGroup.group) + return { + ...chatReply, + lastMessageNewFormat: + formattedReply !== reply ? formattedReply : undefined, + } } const processAndSaveAnswer = (state: SessionState, block: InputBlock, itemId?: string) => - async (reply: string | null): Promise => { + async (reply: string | undefined): Promise => { if (!reply) return state let newState = await saveAnswer(state, block, itemId)(reply) newState = saveVariableValueIfAny(newState, block)(reply) @@ -236,7 +257,7 @@ const getOutgoingEdgeId = ({ typebot: { variables } }: Pick) => ( block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock, - reply: string | null + reply: string | undefined ) => { if ( block.type === InputBlockType.CHOICE && @@ -264,8 +285,8 @@ const getOutgoingEdgeId = export const formatReply = ( inputValue: string | undefined, blockType: BlockType -): string | null => { - if (!inputValue) return null +): string | undefined => { + if (!inputValue) return switch (blockType) { case InputBlockType.PHONE: return formatPhoneNumber(inputValue) diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index d0c19cae4..79fef8319 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.1.15", + "version": "0.1.16", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx b/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx index 73e841290..d09b081b5 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx @@ -93,8 +93,6 @@ export const ChatChunk = (props: Props) => { ref={inputRef} block={props.input} inputIndex={props.inputIndex} - onSubmit={props.onSubmit} - onSkip={props.onSkip} hasHostAvatar={props.theme.chat.hostAvatar?.isEnabled ?? false} guestAvatar={props.theme.chat.guestAvatar} context={props.context} @@ -102,6 +100,8 @@ export const ChatChunk = (props: Props) => { props.settings.general.isInputPrefillEnabled ?? true } hasError={props.hasError} + onSubmit={props.onSubmit} + onSkip={props.onSkip} /> )} diff --git a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx index abe640c66..648c5d35b 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -21,6 +21,10 @@ import { executeClientSideAction } from '@/utils/executeClientSideActions' import { LoadingChunk } from './LoadingChunk' import { PopupBlockedToast } from './PopupBlockedToast' import { setStreamingMessage } from '@/utils/streamingMessageSignal' +import { + formattedMessages, + setFormattedMessages, +} from '@/utils/formattedMessagesSignal' const parseDynamicTheme = ( initialTheme: Theme, @@ -164,6 +168,15 @@ export const ConversationContainer = (props: Props) => { ]) } if (!data) return + if (data.lastMessageNewFormat) { + setFormattedMessages([ + ...formattedMessages(), + { + inputId: [...chatChunks()].pop()?.input?.id ?? '', + formattedMessage: data.lastMessageNewFormat as string, + }, + ]) + } if (data.logs) props.onNewLogs?.(data.logs) if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme) if (data.input?.id && props.onNewInputBlock) { diff --git a/packages/embeds/js/src/components/InputChatBlock.tsx b/packages/embeds/js/src/components/InputChatBlock.tsx index e3753e4e4..0cbf82adf 100644 --- a/packages/embeds/js/src/components/InputChatBlock.tsx +++ b/packages/embeds/js/src/components/InputChatBlock.tsx @@ -25,7 +25,7 @@ import { PhoneInput } from '@/features/blocks/inputs/phone' import { DateForm } from '@/features/blocks/inputs/date' import { RatingForm } from '@/features/blocks/inputs/rating' import { FileUploadForm } from '@/features/blocks/inputs/fileUpload' -import { createSignal, Switch, Match } from 'solid-js' +import { createSignal, Switch, Match, createEffect } from 'solid-js' import { isNotDefined } from '@typebot.io/lib' import { isMobile } from '@/utils/isMobileSignal' import { PaymentForm } from '@/features/blocks/inputs/payment' @@ -33,6 +33,7 @@ import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons' import { SinglePictureChoice } from '@/features/blocks/inputs/pictureChoice/SinglePictureChoice' import { MultiplePictureChoice } from '@/features/blocks/inputs/pictureChoice/MultiplePictureChoice' +import { formattedMessages } from '@/utils/formattedMessagesSignal' type Props = { ref: HTMLDivElement | undefined @@ -49,6 +50,7 @@ type Props = { export const InputChatBlock = (props: Props) => { const [answer, setAnswer] = createSignal() + const [formattedMessage, setFormattedMessage] = createSignal() const handleSubmit = async ({ label, value }: InputSubmitContent) => { setAnswer(label ?? value) @@ -60,11 +62,18 @@ export const InputChatBlock = (props: Props) => { props.onSkip() } + createEffect(() => { + const formattedMessage = formattedMessages().find( + (message) => message.inputId === props.block.id + )?.formattedMessage + if (formattedMessage) setFormattedMessage(formattedMessage) + }) + return ( diff --git a/packages/embeds/js/src/utils/formattedMessagesSignal.ts b/packages/embeds/js/src/utils/formattedMessagesSignal.ts new file mode 100644 index 000000000..29ac208a7 --- /dev/null +++ b/packages/embeds/js/src/utils/formattedMessagesSignal.ts @@ -0,0 +1,5 @@ +import { createSignal } from 'solid-js' + +export const [formattedMessages, setFormattedMessages] = createSignal< + { inputId: string; formattedMessage: string }[] +>([]) diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 63f5beed4..61167e468 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.1.15", + "version": "0.1.16", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index a8889af0e..600e261b3 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.1.15", + "version": "0.1.16", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/schemas/features/chat.ts b/packages/schemas/features/chat.ts index 07f3f326f..76a435986 100644 --- a/packages/schemas/features/chat.ts +++ b/packages/schemas/features/chat.ts @@ -292,6 +292,12 @@ export const chatReplySchema = z.object({ resultId: z.string().optional(), dynamicTheme: dynamicThemeSchema.optional(), logs: z.array(replyLogSchema).optional(), + lastMessageNewFormat: z + .string() + .optional() + .describe( + 'The sent message is validated and formatted on the backend. This is set only if the message differs from the formatted version.' + ), }) export type ChatSession = z.infer