Skip to content

Commit

Permalink
feat(steps): ✨ Add Embed bubble
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Mar 23, 2022
1 parent c01ffa3 commit 953b95d
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 16 deletions.
3 changes: 3 additions & 0 deletions apps/builder/components/editor/StepsSideBar/StepIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FlagIcon,
GlobeIcon,
ImageIcon,
LayoutIcon,
NumberIcon,
PhoneIcon,
SendEmailIcon,
Expand All @@ -39,6 +40,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
return <ImageIcon color="blue.500" {...props} />
case BubbleStepType.VIDEO:
return <FilmIcon color="blue.500" {...props} />
case BubbleStepType.EMBED:
return <LayoutIcon color="blue.500" {...props} />
case InputStepType.TEXT:
return <TextIcon color="orange.500" {...props} />
case InputStepType.NUMBER:
Expand Down
6 changes: 6 additions & 0 deletions apps/builder/components/editor/StepsSideBar/StepTypeLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export const StepTypeLabel = ({ type }: Props) => {
return <Text>Image</Text>
case BubbleStepType.VIDEO:
return <Text>Video</Text>
case BubbleStepType.EMBED:
return (
<Tooltip label="Embed a pdf, an iframe, a website...">
<Text>Embed</Text>
</Tooltip>
)
case InputStepType.NUMBER:
return <Text>Number</Text>
case InputStepType.EMAIL:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { HStack, Stack, Text } from '@chakra-ui/react'
import { SmartNumberInput } from 'components/shared/SmartNumberInput'
import { Input } from 'components/shared/Textbox/Input'
import { EmbedBubbleContent } from 'models'
import { sanitizeUrl } from 'utils'

type Props = {
content: EmbedBubbleContent
onSubmit: (content: EmbedBubbleContent) => void
}

export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
const handleUrlChange = (url: string) => {
let iframeUrl = sanitizeUrl(
url.trim().startsWith('<iframe') ? extractUrlFromIframe(url) : url
)
if (iframeUrl.endsWith('.pdf')) {
iframeUrl = `https://docs.google.com/viewer?embedded=true&url=${iframeUrl}`
}
onSubmit({ ...content, url: iframeUrl })
}

const handleHeightChange = (height?: number) =>
height && onSubmit({ ...content, height })

return (
<Stack p="2" spacing={6}>
<Stack>
<Input
placeholder="Paste the link or code..."
defaultValue={content?.url ?? ''}
onChange={handleUrlChange}
/>
<Text fontSize="sm" color="gray.400" textAlign="center">
Works with PDFs, iframes, websites...
</Text>
</Stack>

<HStack justify="space-between">
<Text>Height: </Text>
<SmartNumberInput
value={content?.height}
onValueChange={handleHeightChange}
/>
</HStack>
</Stack>
)
}

const extractUrlFromIframe = (iframe: string) =>
[...iframe.matchAll(/src="([^"]+)"/g)][0][1]
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TextBubbleStep,
} from 'models'
import { useRef } from 'react'
import { EmbedUploadContent } from './EmbedUploadContent'
import { VideoUploadContent } from './VideoUploadContent'

type Props = {
Expand All @@ -25,7 +26,10 @@ export const MediaBubblePopoverContent = (props: Props) => {

return (
<Portal>
<PopoverContent onMouseDown={handleMouseDown} w="500px">
<PopoverContent
onMouseDown={handleMouseDown}
w={props.step.type === BubbleStepType.IMAGE ? '500px' : '400px'}
>
<PopoverArrow />
<PopoverBody ref={ref} shadow="lg">
<MediaBubbleContent {...props} />
Expand All @@ -52,5 +56,10 @@ export const MediaBubbleContent = ({ step, onContentChange }: Props) => {
<VideoUploadContent content={step.content} onSubmit={onContentChange} />
)
}
case BubbleStepType.EMBED: {
return (
<EmbedUploadContent content={step.content} onSubmit={onContentChange} />
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { isChoiceInput, isInputStep } from 'utils'
import { ItemNodesList } from '../../ItemNode'
import {
EmbedBubbleContent,
SetVariableContent,
TextBubbleContent,
VideoBubbleContent,
Expand Down Expand Up @@ -42,6 +43,9 @@ export const StepNodeContent = ({ step, indices }: Props) => {
case BubbleStepType.VIDEO: {
return <VideoBubbleContent step={step} />
}
case BubbleStepType.EMBED: {
return <EmbedBubbleContent step={step} />
}
case InputStepType.TEXT: {
return (
<PlaceholderContent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Box, Text } from '@chakra-ui/react'
import { EmbedBubbleStep } from 'models'

export const EmbedBubbleContent = ({ step }: { step: EmbedBubbleStep }) => {
if (!step.content?.url) return <Text color="gray.500">Click to edit...</Text>
return (
<Box w="full" h="120px" pos="relative">
<iframe
id="embed-bubble-content"
src={step.content.url}
style={{
width: '100%',
height: '100%',
pointerEvents: 'none',
borderRadius: '5px',
}}
/>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './WithVariableContent'
export * from './VideoBubbleContent'
export * from './WebhookContent'
export * from './TextBubbleContent'
export * from './EmbedBubbleContent'
74 changes: 74 additions & 0 deletions apps/builder/playwright/tests/bubbles/embed.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import test, { expect } from '@playwright/test'
import {
createTypebots,
parseDefaultBlockWithStep,
} from '../../services/database'
import { BubbleStepType, defaultEmbedBubbleContent } from 'models'
import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid'

const pdfSrc = 'https://www.orimi.com/pdf-test.pdf'
const iframeCode = '<iframe src="https://typebot.io"></iframe>'
const siteSrc = 'https://app.cal.com/baptistearno/15min'

test.describe.parallel('Embed bubble step', () => {
test.describe('Content settings', () => {
test('should import and parse embed correctly', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
...parseDefaultBlockWithStep({
type: BubbleStepType.EMBED,
content: defaultEmbedBubbleContent,
}),
},
])

await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Click to edit...')
await page.fill('input[placeholder="Paste the link or code..."]', pdfSrc)
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
'src',
`https://docs.google.com/viewer?embedded=true&url=${pdfSrc}`
)
await page.fill(
'input[placeholder="Paste the link or code..."]',
iframeCode
)
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
'src',
'https://typebot.io'
)
await page.fill('input[placeholder="Paste the link or code..."]', siteSrc)
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
'src',
siteSrc
)
})
})

test.describe('Preview', () => {
test('should display embed correctly', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
...parseDefaultBlockWithStep({
type: BubbleStepType.EMBED,
content: {
url: siteSrc,
height: 700,
},
}),
},
])

await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Preview')
await expect(
typebotViewer(page).locator('iframe#embed-bubble-content')
).toHaveAttribute('src', siteSrc)
})
})
})
3 changes: 3 additions & 0 deletions apps/builder/services/typebots/typebots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ItemType,
defaultConditionContent,
defaultSendEmailOptions,
defaultEmbedBubbleContent,
} from 'models'
import { Typebot } from 'models'
import useSWR from 'swr'
Expand Down Expand Up @@ -250,6 +251,8 @@ const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => {
return defaultImageBubbleContent
case BubbleStepType.VIDEO:
return defaultVideoBubbleContent
case BubbleStepType.EMBED:
return defaultEmbedBubbleContent
}
}

Expand Down
3 changes: 2 additions & 1 deletion apps/builder/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"composite": true
"composite": true,
"downlevelIteration": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
37 changes: 24 additions & 13 deletions packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,12 @@ const ChatChunks = ({
const avatarSideContainerRef = useRef<any>()

useEffect(() => {
avatarSideContainerRef.current?.refreshTopOffset()
refreshTopOffset()
})

const refreshTopOffset = () =>
avatarSideContainerRef.current?.refreshTopOffset()

return (
<>
<div className="flex">
Expand All @@ -209,18 +212,26 @@ const ChatChunks = ({
hostAvatarSrc={hostAvatar.src}
/>
)}
<TransitionGroup>
{bubbles.map((step) => (
<CSSTransition
key={step.id}
classNames="bubble"
timeout={500}
unmountOnExit
>
<HostBubble step={step} onTransitionEnd={onDisplayNextStep} />
</CSSTransition>
))}
</TransitionGroup>
<div className="flex-1">
<TransitionGroup>
{bubbles.map((step) => (
<CSSTransition
key={step.id}
classNames="bubble"
timeout={500}
unmountOnExit
>
<HostBubble
step={step}
onTransitionEnd={() => {
onDisplayNextStep()
refreshTopOffset()
}}
/>
</CSSTransition>
))}
</TransitionGroup>
</div>
</div>
<CSSTransition
classNames="bubble"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useEffect, useRef, useState } from 'react'
import { EmbedBubbleStep } from 'models'
import { TypingContent } from './TypingContent'

type Props = {
step: EmbedBubbleStep
onTransitionEnd: () => void
}

export const showAnimationDuration = 400

export const EmbedBubble = ({ step, onTransitionEnd }: Props) => {
const messageContainer = useRef<HTMLDivElement | null>(null)
const [isTyping, setIsTyping] = useState(true)

useEffect(() => {
showContentAfterMediaLoad()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const showContentAfterMediaLoad = () => {
setTimeout(() => {
setIsTyping(false)
onTypingEnd()
}, 1000)
}

const onTypingEnd = () => {
setIsTyping(false)
setTimeout(() => {
onTransitionEnd()
}, showAnimationDuration)
}

return (
<div className="flex flex-col w-full" ref={messageContainer}>
<div className="flex mb-2 w-full lg:w-11/12 items-center">
<div
className={
'flex relative z-10 items-start typebot-host-bubble w-full'
}
>
<div
className="flex items-center absolute px-4 py-2 rounded-lg bubble-typing z-10 "
style={{
width: isTyping ? '4rem' : '100%',
height: isTyping ? '2rem' : '100%',
}}
>
{isTyping ? <TypingContent /> : <></>}
</div>
<iframe
id="embed-bubble-content"
src={step.content.url}
className={
'w-full z-20 p-4 content-opacity ' +
(isTyping ? 'opacity-0' : 'opacity-100')
}
style={{
height: isTyping ? '2rem' : step.content.height,
borderRadius: '15px',
}}
/>
</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BubbleStep, BubbleStepType } from 'models'
import React from 'react'
import { EmbedBubble } from './EmbedBubble'
import { ImageBubble } from './ImageBubble'
import { TextBubble } from './TextBubble'
import { VideoBubble } from './VideoBubble'
Expand All @@ -17,5 +18,7 @@ export const HostBubble = ({ step, onTransitionEnd }: Props) => {
return <ImageBubble step={step} onTransitionEnd={onTransitionEnd} />
case BubbleStepType.VIDEO:
return <VideoBubble step={step} onTransitionEnd={onTransitionEnd} />
case BubbleStepType.EMBED:
return <EmbedBubble step={step} onTransitionEnd={onTransitionEnd} />
}
}
Loading

3 comments on commit 953b95d

@vercel
Copy link

@vercel vercel bot commented on 953b95d Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
builder-v2-git-main-typebot-io.vercel.app
app.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 953b95d Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.