diff --git a/apps/builder/src/features/editor/providers/TypebotProvider/actions/edges.ts b/apps/builder/src/features/editor/providers/TypebotProvider/actions/edges.ts index 490bd995d..69542e55f 100644 --- a/apps/builder/src/features/editor/providers/TypebotProvider/actions/edges.ts +++ b/apps/builder/src/features/editor/providers/TypebotProvider/actions/edges.ts @@ -108,17 +108,18 @@ const deleteOutgoingEdgeIdProps = ( const block = typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as | Block | undefined + if (!block) return const fromItemIndex = - edge.from.itemId && block && blockHasItems(block) + edge.from.itemId && blockHasItems(block) ? block.items.findIndex(byId(edge.from.itemId)) : -1 - if (fromBlockIndex !== -1) - typebot.groups[fromGroupIndex].blocks[fromBlockIndex].outgoingEdgeId = - undefined - if (fromItemIndex !== -1) - ( + if (fromItemIndex !== -1) { + ;( typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as BlockWithItems ).items[fromItemIndex].outgoingEdgeId = undefined + } else if (fromBlockIndex !== -1) + typebot.groups[fromGroupIndex].blocks[fromBlockIndex].outgoingEdgeId = + undefined } export const cleanUpEdgeDraft = ( diff --git a/apps/viewer/src/features/blocks/integrations/chatwoot/api/utils/executeChatwootBlock.ts b/apps/viewer/src/features/blocks/integrations/chatwoot/api/utils/executeChatwootBlock.ts index 2df816a2d..032717227 100644 --- a/apps/viewer/src/features/blocks/integrations/chatwoot/api/utils/executeChatwootBlock.ts +++ b/apps/viewer/src/features/blocks/integrations/chatwoot/api/utils/executeChatwootBlock.ts @@ -56,21 +56,23 @@ export const executeChatwootBlock = ( const chatwootCode = parseChatwootOpenCode(block.options) return { outgoingEdgeId: block.outgoingEdgeId, - integrations: { - chatwoot: { - codeToExecute: { - content: parseVariables(variables, { fieldToParse: 'id' })( - chatwootCode - ), - args: extractVariablesFromText(variables)(chatwootCode).map( - (variable) => ({ - id: variable.id, - value: parseCorrectValueType(variable.value), - }) - ), + clientSideActions: [ + { + chatwoot: { + codeToExecute: { + content: parseVariables(variables, { fieldToParse: 'id' })( + chatwootCode + ), + args: extractVariablesFromText(variables)(chatwootCode).map( + (variable) => ({ + id: variable.id, + value: parseCorrectValueType(variable.value), + }) + ), + }, }, }, - }, + ], logs: isPreview ? [ { diff --git a/apps/viewer/src/features/blocks/integrations/googleAnalytics/api/utils/executeGoogleAnalyticsBlock.ts b/apps/viewer/src/features/blocks/integrations/googleAnalytics/api/utils/executeGoogleAnalyticsBlock.ts index 9c22d06ff..8928af9f8 100644 --- a/apps/viewer/src/features/blocks/integrations/googleAnalytics/api/utils/executeGoogleAnalyticsBlock.ts +++ b/apps/viewer/src/features/blocks/integrations/googleAnalytics/api/utils/executeGoogleAnalyticsBlock.ts @@ -7,7 +7,9 @@ export const executeGoogleAnalyticsBlock = ( block: GoogleAnalyticsBlock ): ExecuteIntegrationResponse => ({ outgoingEdgeId: block.outgoingEdgeId, - integrations: { - googleAnalytics: deepParseVariable(variables)(block.options), - }, + clientSideActions: [ + { + googleAnalytics: deepParseVariable(variables)(block.options), + }, + ], }) diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/api/utils/insertRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/api/utils/insertRow.ts index 16d52cf98..3ca4cb50a 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/api/utils/insertRow.ts +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/api/utils/insertRow.ts @@ -10,7 +10,6 @@ export const insertRow = async ( options, }: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions } ): Promise => { - console.log('insertRow', options) if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId } let log: ReplyLog | undefined diff --git a/apps/viewer/src/features/blocks/logic/code/api/utils/executeCode.ts b/apps/viewer/src/features/blocks/logic/code/api/utils/executeCode.ts index da53a3edf..671af2a5d 100644 --- a/apps/viewer/src/features/blocks/logic/code/api/utils/executeCode.ts +++ b/apps/viewer/src/features/blocks/logic/code/api/utils/executeCode.ts @@ -24,11 +24,13 @@ export const executeCode = ( return { outgoingEdgeId: block.outgoingEdgeId, - logic: { - codeToExecute: { - content, - args, + clientSideActions: [ + { + codeToExecute: { + content, + args, + }, }, - }, + ], } } diff --git a/apps/viewer/src/features/blocks/logic/redirect/api/utils/executeRedirect.ts b/apps/viewer/src/features/blocks/logic/redirect/api/utils/executeRedirect.ts index 9018ce8d6..0188085ce 100644 --- a/apps/viewer/src/features/blocks/logic/redirect/api/utils/executeRedirect.ts +++ b/apps/viewer/src/features/blocks/logic/redirect/api/utils/executeRedirect.ts @@ -10,9 +10,11 @@ export const executeRedirect = ( if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId } const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url)) return { - logic: { - redirect: { url: formattedUrl, isNewTab: block.options.isNewTab }, - }, + clientSideActions: [ + { + redirect: { url: formattedUrl, isNewTab: block.options.isNewTab }, + }, + ], outgoingEdgeId: block.outgoingEdgeId, } } diff --git a/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts b/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts index 1e6dc4912..f331f1169 100644 --- a/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts +++ b/apps/viewer/src/features/chat/api/procedures/sendMessageProcedure.ts @@ -48,6 +48,7 @@ export const sendMessageProcedure = publicProcedure resultId, dynamicTheme, logs, + clientSideActions, } = await startSession(startParams) return { sessionId, @@ -63,9 +64,10 @@ export const sendMessageProcedure = publicProcedure resultId, dynamicTheme, logs, + clientSideActions, } } else { - const { messages, input, logic, newSessionState, integrations, logs } = + const { messages, input, clientSideActions, newSessionState, logs } = await continueBotFlow(session.state)(message) await prisma.chatSession.updateMany({ @@ -78,8 +80,7 @@ export const sendMessageProcedure = publicProcedure return { messages, input, - logic, - integrations, + clientSideActions, dynamicTheme: parseDynamicThemeReply(newSessionState), logs, } @@ -133,7 +134,7 @@ const startSession = async (startParams?: StartParams) => { const { messages, input, - logic, + clientSideActions, newSessionState: newInitialState, logs, } = await startBotFlow(initialState, startParams.startGroupId) @@ -141,7 +142,7 @@ const startSession = async (startParams?: StartParams) => { if (!input) return { messages, - logic, + clientSideActions, typebot: { id: typebot.id, settings: deepParseVariable(newInitialState.typebot.variables)( @@ -183,7 +184,7 @@ const startSession = async (startParams?: StartParams) => { }, messages, input, - logic, + clientSideActions, dynamicTheme: parseDynamicThemeReply(newInitialState), logs, } satisfies ChatReply diff --git a/apps/viewer/src/features/chat/api/utils/executeGroup.ts b/apps/viewer/src/features/chat/api/utils/executeGroup.ts index 599d87fc1..57ea63f33 100644 --- a/apps/viewer/src/features/chat/api/utils/executeGroup.ts +++ b/apps/viewer/src/features/chat/api/utils/executeGroup.ts @@ -25,8 +25,8 @@ export const executeGroup = group: Group ): Promise => { const messages: ChatReply['messages'] = currentReply?.messages ?? [] - let logic: ChatReply['logic'] = currentReply?.logic - let integrations: ChatReply['integrations'] = currentReply?.integrations + let clientSideActions: ChatReply['clientSideActions'] = + currentReply?.clientSideActions let logs: ChatReply['logs'] = currentReply?.logs let nextEdgeId = null @@ -59,8 +59,7 @@ export const executeGroup = blockId: block.id, }, }, - logic, - integrations, + clientSideActions, logs, } const executionResponse = isLogicBlock(block) @@ -70,10 +69,14 @@ export const executeGroup = : null if (!executionResponse) continue - if ('logic' in executionResponse && executionResponse.logic) - logic = { ...logic, ...executionResponse.logic } - if ('integrations' in executionResponse && executionResponse.integrations) - integrations = { ...integrations, ...executionResponse.integrations } + if ( + 'clientSideActions' in executionResponse && + executionResponse.clientSideActions + ) + clientSideActions = [ + ...(clientSideActions ?? []), + ...executionResponse.clientSideActions, + ] if (executionResponse.logs) logs = [...(logs ?? []), ...executionResponse.logs] if (executionResponse.newSessionState) @@ -85,20 +88,19 @@ export const executeGroup = } if (!nextEdgeId) - return { messages, newSessionState, logic, integrations, logs } + return { messages, newSessionState, clientSideActions, logs } const nextGroup = getNextGroup(newSessionState)(nextEdgeId) if (nextGroup?.updatedContext) newSessionState = nextGroup.updatedContext if (!nextGroup) { - return { messages, newSessionState, logic, integrations, logs } + return { messages, newSessionState, clientSideActions, logs } } return executeGroup(newSessionState, { messages, - logic, - integrations, + clientSideActions, logs, })(nextGroup.group) } diff --git a/apps/viewer/src/features/chat/types.ts b/apps/viewer/src/features/chat/types.ts index 10ea9f804..01df4a31a 100644 --- a/apps/viewer/src/features/chat/types.ts +++ b/apps/viewer/src/features/chat/types.ts @@ -5,9 +5,9 @@ export type EdgeId = string export type ExecuteLogicResponse = { outgoingEdgeId: EdgeId | undefined newSessionState?: SessionState -} & Pick +} & Pick export type ExecuteIntegrationResponse = { outgoingEdgeId: EdgeId | undefined newSessionState?: SessionState -} & Pick +} & Pick diff --git a/packages/js/src/components/ConversationContainer/ChatChunk.tsx b/packages/js/src/components/ConversationContainer/ChatChunk.tsx index c4ae1b914..47f978471 100644 --- a/packages/js/src/components/ConversationContainer/ChatChunk.tsx +++ b/packages/js/src/components/ConversationContainer/ChatChunk.tsx @@ -12,14 +12,17 @@ type Props = Pick & { context: BotContext onScrollToBottom: () => void onSubmit: (input: string) => void - onEnd?: () => void onSkip: () => void + onAllBubblesDisplayed: () => void } export const ChatChunk = (props: Props) => { const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(0) onMount(() => { + if (props.messages.length === 0) { + props.onAllBubblesDisplayed() + } props.onScrollToBottom() }) @@ -30,8 +33,9 @@ export const ChatChunk = (props: Props) => { : displayedMessageIndex() + 1 ) props.onScrollToBottom() - if (!props.input && displayedMessageIndex() === props.messages.length) - return props.onEnd?.() + if (displayedMessageIndex() === props.messages.length) { + props.onAllBubblesDisplayed() + } } return ( diff --git a/packages/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/js/src/components/ConversationContainer/ConversationContainer.tsx index 3861a8a36..1b1cff3e4 100644 --- a/packages/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -3,8 +3,8 @@ import { createEffect, createSignal, For } from 'solid-js' import { sendMessageQuery } from '@/queries/sendMessageQuery' import { ChatChunk } from './ChatChunk' import { BotContext, InitialChatReply } from '@/types' -import { executeIntegrations } from '@/utils/executeIntegrations' -import { executeLogic } from '@/utils/executeLogic' +import { isNotDefined } from 'utils' +import { executeClientSideAction } from '@/utils/executeClientSideActions' const parseDynamicTheme = ( initialTheme: Theme, @@ -42,10 +42,13 @@ type Props = { export const ConversationContainer = (props: Props) => { let chatContainer: HTMLDivElement | undefined let bottomSpacer: HTMLDivElement | undefined - const [chatChunks, setChatChunks] = createSignal([ + const [chatChunks, setChatChunks] = createSignal< + Pick[] + >([ { input: props.initialChatReply.input, messages: props.initialChatReply.messages, + clientSideActions: props.initialChatReply.clientSideActions, }, ]) const [dynamicTheme, setDynamicTheme] = createSignal< @@ -77,17 +80,12 @@ export const ConversationContainer = (props: Props) => { groupId: data.input.groupId, }) } - if (data.integrations) { - executeIntegrations(data.integrations) - } - if (data.logic) { - await executeLogic(data.logic) - } setChatChunks((displayedChunks) => [ ...displayedChunks, { input: data.input, messages: data.messages, + clientSideActions: data.clientSideActions, }, ]) } @@ -99,6 +97,19 @@ export const ConversationContainer = (props: Props) => { }, 50) } + const handleAllBubblesDisplayed = async () => { + const lastChunk = chatChunks().at(-1) + if (!lastChunk) return + if (lastChunk.clientSideActions) { + for (const action of lastChunk.clientSideActions) { + await executeClientSideAction(action) + } + } + if (isNotDefined(lastChunk.input)) { + props.onEnd?.() + } + } + return (
{ input={chatChunk.input} theme={theme()} settings={props.initialChatReply.typebot.settings} + onAllBubblesDisplayed={handleAllBubblesDisplayed} onSubmit={sendMessage} onScrollToBottom={autoScrollToBottom} onSkip={() => { // TODO: implement skip }} - onEnd={props.onEnd} context={props.context} /> )} diff --git a/packages/js/src/utils/executeClientSideActions.ts b/packages/js/src/utils/executeClientSideActions.ts new file mode 100644 index 000000000..09461700b --- /dev/null +++ b/packages/js/src/utils/executeClientSideActions.ts @@ -0,0 +1,22 @@ +import { executeChatwoot } from '@/features/blocks/integrations/chatwoot' +import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics' +import { executeCode } from '@/features/blocks/logic/code' +import { executeRedirect } from '@/features/blocks/logic/redirect' +import type { ChatReply } from 'models' + +export const executeClientSideAction = async ( + clientSideAction: NonNullable[0] +) => { + if ('chatwoot' in clientSideAction) { + executeChatwoot(clientSideAction.chatwoot) + } + if ('googleAnalytics' in clientSideAction) { + executeGoogleAnalyticsBlock(clientSideAction.googleAnalytics) + } + if ('codeToExecute' in clientSideAction) { + await executeCode(clientSideAction.codeToExecute) + } + if ('redirect' in clientSideAction) { + executeRedirect(clientSideAction.redirect) + } +} diff --git a/packages/js/src/utils/executeIntegrations.ts b/packages/js/src/utils/executeIntegrations.ts deleted file mode 100644 index 47e1c59e1..000000000 --- a/packages/js/src/utils/executeIntegrations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { executeChatwoot } from '@/features/blocks/integrations/chatwoot' -import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics' -import type { ChatReply } from 'models' - -export const executeIntegrations = async ( - integrations: ChatReply['integrations'] -) => { - if (integrations?.chatwoot?.codeToExecute) { - executeChatwoot(integrations.chatwoot) - } - if (integrations?.googleAnalytics) { - executeGoogleAnalyticsBlock(integrations.googleAnalytics) - } -} diff --git a/packages/js/src/utils/executeLogic.ts b/packages/js/src/utils/executeLogic.ts deleted file mode 100644 index 2d3534b19..000000000 --- a/packages/js/src/utils/executeLogic.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { executeCode } from '@/features/blocks/logic/code' -import { executeRedirect } from '@/features/blocks/logic/redirect' -import type { ChatReply } from 'models' - -export const executeLogic = async (logic: ChatReply['logic']) => { - if (logic?.codeToExecute) { - await executeCode(logic.codeToExecute) - } - if (logic?.redirect) { - executeRedirect(logic.redirect) - } -} diff --git a/packages/models/features/chat.ts b/packages/models/features/chat.ts index ba8b80d82..1d6c088f7 100644 --- a/packages/models/features/chat.ts +++ b/packages/models/features/chat.ts @@ -165,6 +165,26 @@ const replyLogSchema = logSchema }) .and(z.object({ details: z.unknown().optional() })) +const clientSideActionSchema = z + .object({ + codeToExecute: codeToExecuteSchema, + }) + .or( + z.object({ + redirect: redirectOptionsSchema, + }) + ) + .or( + z.object({ + chatwoot: z.object({ codeToExecute: codeToExecuteSchema }), + }) + ) + .or( + z.object({ + googleAnalytics: googleAnalyticsOptionsSchema, + }) + ) + export const chatReplySchema = z.object({ messages: z.array(chatMessageSchema), input: inputBlockSchema @@ -175,22 +195,7 @@ export const chatReplySchema = z.object({ }) ) .optional(), - logic: z - .object({ - redirect: redirectOptionsSchema.optional(), - codeToExecute: codeToExecuteSchema.optional(), - }) - .optional(), - integrations: z - .object({ - chatwoot: z - .object({ - codeToExecute: codeToExecuteSchema, - }) - .optional(), - googleAnalytics: googleAnalyticsOptionsSchema.optional(), - }) - .optional(), + clientSideActions: z.array(clientSideActionSchema).optional(), sessionId: z.string().optional(), typebot: typebotSchema .pick({ id: true, theme: true, settings: true })