⚡ Improve new bot engine client side actions
We make sure to save client side actions in an array that will be executed sequentially
This commit is contained in:
@ -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 = (
|
||||
|
@ -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
|
||||
? [
|
||||
{
|
||||
|
@ -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),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -10,7 +10,6 @@ export const insertRow = async (
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
console.log('insertRow', options)
|
||||
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
|
||||
|
||||
let log: ReplyLog | undefined
|
||||
|
@ -24,11 +24,13 @@ export const executeCode = (
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logic: {
|
||||
codeToExecute: {
|
||||
content,
|
||||
args,
|
||||
clientSideActions: [
|
||||
{
|
||||
codeToExecute: {
|
||||
content,
|
||||
args,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -25,8 +25,8 @@ export const executeGroup =
|
||||
group: Group
|
||||
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||
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)
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ export type EdgeId = string
|
||||
export type ExecuteLogicResponse = {
|
||||
outgoingEdgeId: EdgeId | undefined
|
||||
newSessionState?: SessionState
|
||||
} & Pick<ChatReply, 'logic' | 'logs'>
|
||||
} & Pick<ChatReply, 'clientSideActions' | 'logs'>
|
||||
|
||||
export type ExecuteIntegrationResponse = {
|
||||
outgoingEdgeId: EdgeId | undefined
|
||||
newSessionState?: SessionState
|
||||
} & Pick<ChatReply, 'integrations' | 'logs'>
|
||||
} & Pick<ChatReply, 'clientSideActions' | 'logs'>
|
||||
|
@ -12,14 +12,17 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
|
||||
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 (
|
||||
|
@ -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<ChatReply[]>([
|
||||
const [chatChunks, setChatChunks] = createSignal<
|
||||
Pick<ChatReply, 'messages' | 'input' | 'clientSideActions'>[]
|
||||
>([
|
||||
{
|
||||
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 (
|
||||
<div
|
||||
ref={chatContainer}
|
||||
@ -112,12 +123,12 @@ export const ConversationContainer = (props: Props) => {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
22
packages/js/src/utils/executeClientSideActions.ts
Normal file
22
packages/js/src/utils/executeClientSideActions.ts
Normal file
@ -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<ChatReply['clientSideActions']>[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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 })
|
||||
|
Reference in New Issue
Block a user