2
0

(embed) Option to add a wait event for the embed bubble

Closes #1590
This commit is contained in:
Baptiste Arnaud
2024-06-19 15:27:45 +02:00
parent 4ab1803d39
commit 918836d6cf
20 changed files with 200 additions and 46 deletions

View File

@ -15,25 +15,23 @@ const defaultItem = {
id: createId(),
}
type ItemWithId<T> = T & { id: string }
export type TableListItemProps<T> = {
item: T
onItemChange: (item: T) => void
}
type Props<T> = {
initialItems?: ItemWithId<T>[]
type Props<T extends object> = {
initialItems?: T[]
isOrdered?: boolean
addLabel?: string
newItemDefaultProps?: Partial<T>
hasDefaultItem?: boolean
ComponentBetweenItems?: (props: unknown) => JSX.Element
onItemsChange: (items: ItemWithId<T>[]) => void
onItemsChange: (items: T[]) => void
children: (props: TableListItemProps<T>) => JSX.Element
}
export const TableList = <T,>({
export const TableList = <T extends object>({
initialItems,
isOrdered,
addLabel = 'Add',
@ -45,7 +43,7 @@ export const TableList = <T,>({
}: Props<T>) => {
const [items, setItems] = useState(
addIdsIfMissing(initialItems) ??
(hasDefaultItem ? ([defaultItem] as ItemWithId<T>[]) : [])
(hasDefaultItem ? ([defaultItem] as T[]) : [])
)
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
@ -56,14 +54,14 @@ export const TableList = <T,>({
const createItem = () => {
const id = createId()
const newItem = { id, ...newItemDefaultProps } as ItemWithId<T>
const newItem = { id, ...newItemDefaultProps } as T
setItems([...items, newItem])
onItemsChange([...items, newItem])
}
const insertItem = (itemIndex: number) => () => {
const id = createId()
const newItem = { id } as ItemWithId<T>
const newItem = { id } as T
const newItems = [...items]
newItems.splice(itemIndex + 1, 0, newItem)
setItems(newItems)
@ -96,7 +94,7 @@ export const TableList = <T,>({
return (
<Stack spacing={0}>
{items.map((item, itemIndex) => (
<Box key={item.id}>
<Box key={'id' in item ? (item.id as string) : itemIndex}>
{itemIndex !== 0 && ComponentBetweenItems && (
<ComponentBetweenItems />
)}
@ -185,7 +183,7 @@ export const TableList = <T,>({
)
}
const addIdsIfMissing = <T,>(items?: T[]): ItemWithId<T>[] | undefined =>
const addIdsIfMissing = <T,>(items?: T[]): T[] | undefined =>
items?.map((item) => ({
id: createId(),
...item,

View File

@ -1,14 +1,29 @@
import { useTranslate } from '@tolgee/react'
import { Text } from '@chakra-ui/react'
import { Stack, Text } from '@chakra-ui/react'
import { EmbedBubbleBlock } from '@typebot.io/schemas'
import { SetVariableLabel } from '@/components/SetVariableLabel'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
type Props = {
block: EmbedBubbleBlock
}
export const EmbedBubbleContent = ({ block }: Props) => {
const { typebot } = useTypebot()
const { t } = useTranslate()
if (!block.content?.url)
return <Text color="gray.500">{t('clickToEdit')}</Text>
return <Text>{t('editor.blocks.bubbles.embed.node.show.text')}</Text>
return (
<Stack>
<Text>{t('editor.blocks.bubbles.embed.node.show.text')}</Text>
{typebot &&
block.content.waitForEvent?.isEnabled &&
block.content.waitForEvent.saveDataInVariableId && (
<SetVariableLabel
variables={typebot.variables}
variableId={block.content.waitForEvent.saveDataInVariableId}
/>
)}
</Stack>
)
}

View File

@ -1,9 +1,11 @@
import { TextInput, NumberInput } from '@/components/inputs'
import { Stack, Text } from '@chakra-ui/react'
import { EmbedBubbleBlock } from '@typebot.io/schemas'
import { EmbedBubbleBlock, Variable } from '@typebot.io/schemas'
import { sanitizeUrl } from '@typebot.io/lib'
import { useTranslate } from '@tolgee/react'
import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/embed/constants'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
type Props = {
content: EmbedBubbleBlock['content']
@ -23,6 +25,24 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
height?: NonNullable<EmbedBubbleBlock['content']>['height']
) => height && onSubmit({ ...content, height })
const updateWaitEventName = (name: string) =>
onSubmit({ ...content, waitForEvent: { ...content?.waitForEvent, name } })
const updateWaitForEventEnabled = (isEnabled: boolean) =>
onSubmit({
...content,
waitForEvent: { ...content?.waitForEvent, isEnabled },
})
const updateSaveDataInVariableId = (variable?: Pick<Variable, 'id'>) =>
onSubmit({
...content,
waitForEvent: {
...content?.waitForEvent,
saveDataInVariableId: variable?.id,
},
})
return (
<Stack p="2" spacing={6}>
<Stack>
@ -43,8 +63,25 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
defaultValue={content?.height ?? defaultEmbedBubbleContent.height}
onValueChange={handleHeightChange}
suffix={t('editor.blocks.bubbles.embed.settings.numberInput.unit')}
width="150px"
direction="row"
/>
<SwitchWithRelatedSettings
label="Wait for event?"
initialValue={content?.waitForEvent?.isEnabled ?? false}
onCheckChange={updateWaitForEventEnabled}
>
<TextInput
direction="row"
label="Name:"
defaultValue={content?.waitForEvent?.name}
onChange={updateWaitEventName}
/>
<VariableSearchInput
onSelectVariable={updateSaveDataInVariableId}
initialVariableId={content?.waitForEvent?.saveDataInVariableId}
label="Save data in variable"
/>
</SwitchWithRelatedSettings>
</Stack>
)
}

View File

@ -35,3 +35,24 @@ The Embed bubble block allows you to display a website or an iframe to your user
For this, you'll need to select the pdf file you want to embed. Right click > Preview > More actions > Open in a new window. Now click More actions > Embed item.
Copy the embed code and paste it in the Embed bubble block configuration.
## Wait for event
Enable this if you are the owner of the website you want to embed and would like to continue the bot flow only when an event from the embed is sent to the bot. This event dispatch needs to be executed in the embed website. Here is an example:
```js
window.parent.postMessage(
{ name: 'My event', data: 'Custom data passed to the typebot variable' },
'*'
)
```
You can choose the name of the event, it needs to match what you've set in the Embed bubble block configuration.
<Frame>
<img
src="/images/blocks/bubbles/embed-wait.jpg"
alt="Embed bubble"
className="rounded-lg"
/>
</Frame>

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -11,6 +11,7 @@ import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
export const sendMessageV1 = publicProcedure
.meta({
@ -136,8 +137,11 @@ export const sendMessageV1 = publicProcedure
logs: allLogs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
setVariableHistory,
})
@ -199,8 +203,11 @@ export const sendMessageV1 = publicProcedure
logs: allLogs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
setVariableHistory,
})

View File

@ -11,6 +11,7 @@ import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/legacy/schema'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
export const sendMessageV2 = publicProcedure
.meta({
@ -136,8 +137,11 @@ export const sendMessageV2 = publicProcedure
logs: allLogs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
setVariableHistory,
})
@ -198,8 +202,11 @@ export const sendMessageV2 = publicProcedure
logs: allLogs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
setVariableHistory,
})

View File

@ -1,11 +1,12 @@
import { TRPCError } from '@trpc/server'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { isDefined, isNotDefined, isNotEmpty } from '@typebot.io/lib/utils'
import { getSession } from '../queries/getSession'
import { continueBotFlow } from '../continueBotFlow'
import { filterPotentiallySensitiveLogs } from '../logs/filterPotentiallySensitiveLogs'
import { parseDynamicTheme } from '../parseDynamicTheme'
import { saveStateToDatabase } from '../saveStateToDatabase'
import { computeCurrentProgress } from '../computeCurrentProgress'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
type Props = {
origin: string | undefined
@ -77,8 +78,11 @@ export const continueChat = async ({
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
})

View File

@ -1,8 +1,10 @@
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { computeCurrentProgress } from '../computeCurrentProgress'
import { filterPotentiallySensitiveLogs } from '../logs/filterPotentiallySensitiveLogs'
import { restartSession } from '../queries/restartSession'
import { saveStateToDatabase } from '../saveStateToDatabase'
import { startSession } from '../startSession'
import { isNotEmpty } from '@typebot.io/lib'
type Props = {
origin: string | undefined
@ -74,8 +76,11 @@ export const startChat = async ({
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
})

View File

@ -3,6 +3,7 @@ import { restartSession } from '../queries/restartSession'
import { saveStateToDatabase } from '../saveStateToDatabase'
import { startSession } from '../startSession'
import { computeCurrentProgress } from '../computeCurrentProgress'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
type Props = {
message?: string
@ -69,8 +70,11 @@ export const startChatPreview = async ({
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
hasEmbedBubbleWithWaitEvent: messages.some(
(message) =>
message.type === 'custom-embed' ||
(message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled)
),
initialSessionId: sessionId,
})

View File

@ -145,6 +145,13 @@ export const continueBotFlow = async (
}
}
}
} else if (
block.type === BubbleBlockType.EMBED &&
block.content?.waitForEvent?.saveDataInVariableId
) {
variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
(v) => v.id === block.content?.waitForEvent?.saveDataInVariableId
)
}
if (variableToUpdate) {

View File

@ -6,7 +6,7 @@ import {
SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import { isNotEmpty } from '@typebot.io/lib'
import { isEmpty, isNotEmpty } from '@typebot.io/lib'
import {
isBubbleBlock,
isInputBlock,
@ -32,6 +32,7 @@ import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
} from './parseBubbleBlock'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
type ContextProps = {
version: 1 | 2
@ -95,14 +96,30 @@ export const executeGroup = async (
if (isBubbleBlock(block)) {
if (!block.content || (firstBubbleWasStreamed && index === 0)) continue
messages.push(
parseBubbleBlock(block as BubbleBlockWithDefinedContent, {
const message = parseBubbleBlock(block as BubbleBlockWithDefinedContent, {
version,
variables: newSessionState.typebotsQueue[0].typebot.variables,
typebotVersion: newSessionState.typebotsQueue[0].typebot.version,
textBubbleContentFormat,
})
)
messages.push(message)
if (
message.type === BubbleBlockType.EMBED &&
message.content.waitForEvent?.isEnabled
) {
return {
messages,
newSessionState: {
...newSessionState,
currentBlockId: block.id,
},
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
}
lastBubbleBlockId = block.id
continue
}

View File

@ -17,7 +17,7 @@ type Props = {
clientSideActions: ContinueChatResponse['clientSideActions']
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
hasCustomEmbedBubble?: boolean
hasEmbedBubbleWithWaitEvent?: boolean
initialSessionId?: string
}
@ -28,7 +28,7 @@ export const saveStateToDatabase = async ({
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble,
hasEmbedBubbleWithWaitEvent,
initialSessionId,
}: Props) => {
const containsSetVariableClientSideAction = clientSideActions?.some(
@ -36,7 +36,9 @@ export const saveStateToDatabase = async ({
)
const isCompleted = Boolean(
!input && !containsSetVariableClientSideAction && !hasCustomEmbedBubble
!input &&
!containsSetVariableClientSideAction &&
!hasEmbedBubbleWithWaitEvent
)
const queries: Prisma.PrismaPromise<any>[] = []

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.2.88",
"version": "0.2.89",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",

View File

@ -51,6 +51,7 @@ export const HostBubble = (props: Props) => (
<EmbedBubble
content={props.message.content as EmbedBubbleBlock['content']}
onTransitionEnd={props.onTransitionEnd}
onCompleted={props.onCompleted}
/>
</Match>
<Match when={props.message.type === 'custom-embed'}>

View File

@ -4,10 +4,12 @@ import { createSignal, onCleanup, onMount } from 'solid-js'
import { clsx } from 'clsx'
import { EmbedBubbleBlock } from '@typebot.io/schemas'
import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/embed/constants'
import { isNotEmpty } from '@typebot.io/lib/utils'
type Props = {
content: EmbedBubbleBlock['content']
onTransitionEnd?: (ref?: HTMLDivElement) => void
onCompleted?: (data?: string) => void
}
let typingTimeout: NodeJS.Timeout
@ -20,9 +22,29 @@ export const EmbedBubble = (props: Props) => {
props.onTransitionEnd ? true : false
)
const handleMessage = (
event: MessageEvent<{ name?: string; data?: string }>
) => {
if (
props.content?.waitForEvent?.isEnabled &&
isNotEmpty(event.data.name) &&
event.data.name === props.content?.waitForEvent.name
) {
props.onCompleted?.(
props.content.waitForEvent.saveDataInVariableId && event.data.data
? event.data.data
: undefined
)
window.removeEventListener('message', handleMessage)
}
}
onMount(() => {
typingTimeout = setTimeout(() => {
setIsTyping(false)
if (props.content?.waitForEvent?.isEnabled) {
window.addEventListener('message', handleMessage)
}
setTimeout(() => {
props.onTransitionEnd?.(ref)
}, showAnimationDuration)
@ -31,6 +53,7 @@ export const EmbedBubble = (props: Props) => {
onCleanup(() => {
if (typingTimeout) clearTimeout(typingTimeout)
window.removeEventListener('message', handleMessage)
})
return (

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/nextjs",
"version": "0.2.88",
"version": "0.2.89",
"description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/react",
"version": "0.2.88",
"version": "0.2.89",
"description": "Convenient library to display typebots on your React app",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -6,6 +6,13 @@ import { BubbleBlockType } from '../constants'
export const embedBubbleContentSchema = z.object({
url: z.string().optional(),
height: z.number().or(variableStringSchema).optional(),
waitForEvent: z
.object({
isEnabled: z.boolean().optional(),
name: z.string().optional(),
saveDataInVariableId: z.string().optional(),
})
.optional(),
})
export const embedBubbleBlockSchema = blockBaseSchema.merge(

View File

@ -16,7 +16,6 @@ import {
import { logSchema } from '../result'
import { settingsSchema, themeSchema } from '../typebot'
import {
textBubbleContentSchema,
imageBubbleContentSchema,
videoBubbleContentSchema,
audioBubbleContentSchema,