diff --git a/apps/builder/src/components/icons.tsx b/apps/builder/src/components/icons.tsx
index 89938d9af..e9a1990e9 100644
--- a/apps/builder/src/components/icons.tsx
+++ b/apps/builder/src/components/icons.tsx
@@ -678,3 +678,20 @@ export const BracesIcon = (props: IconProps) => (
)
+
+export const VideoPopoverIcon = (props: IconProps) => (
+
+
+
+
+)
diff --git a/apps/builder/src/features/editor/components/BoardMenuButton.tsx b/apps/builder/src/features/editor/components/BoardMenuButton.tsx
index cc8382210..b5e47c266 100644
--- a/apps/builder/src/features/editor/components/BoardMenuButton.tsx
+++ b/apps/builder/src/features/editor/components/BoardMenuButton.tsx
@@ -18,27 +18,19 @@ import {
SettingsIcon,
} from '@/components/icons'
import { useTypebot } from '../providers/TypebotProvider'
-import React, { useEffect, useState } from 'react'
+import React, { useState } from 'react'
import { EditorSettingsModal } from './EditorSettingsModal'
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
import { useTranslate } from '@tolgee/react'
-import { useUser } from '@/features/account/hooks/useUser'
-import { useRouter } from 'next/router'
import { RightPanel, useEditor } from '../providers/EditorProvider'
export const BoardMenuButton = (props: StackProps) => {
- const { query } = useRouter()
const { typebot, currentUserMode } = useTypebot()
- const { user } = useUser()
const [isDownloading, setIsDownloading] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslate()
const { setRightPanel } = useEditor()
- useEffect(() => {
- if (user && !user.graphNavigation && !query.isFirstBot) onOpen()
- }, [onOpen, query.isFirstBot, user, user?.graphNavigation])
-
const downloadFlow = () => {
assert(typebot)
setIsDownloading(true)
diff --git a/apps/builder/src/features/editor/components/EditorPage.tsx b/apps/builder/src/features/editor/components/EditorPage.tsx
index 514a3dd2f..80c03c63a 100644
--- a/apps/builder/src/features/editor/components/EditorPage.tsx
+++ b/apps/builder/src/features/editor/components/EditorPage.tsx
@@ -8,7 +8,6 @@ import {
import { useTypebot } from '../providers/TypebotProvider'
import { BlocksSideBar } from './BlocksSideBar'
import { BoardMenuButton } from './BoardMenuButton'
-import { GettingStartedModal } from './GettingStartedModal'
import { PreviewDrawer } from '@/features/preview/components/PreviewDrawer'
import { TypebotHeader } from './TypebotHeader'
import { Graph } from '@/features/graph/components/Graph'
@@ -19,6 +18,7 @@ import { TypebotNotFoundPage } from './TypebotNotFoundPage'
import { SuspectedTypebotBanner } from './SuspectedTypebotBanner'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { VariablesDrawer } from '@/features/preview/components/VariablesDrawer'
+import { VideoOnboardingFloatingWindow } from '@/features/onboarding/components/VideoOnboardingFloatingWindow'
export const EditorPage = () => {
const { typebot, currentUserMode, is404 } = useTypebot()
@@ -32,11 +32,12 @@ export const EditorPage = () => {
const isSuspicious = typebot?.riskLevel === 100 && !workspace?.isVerified
if (is404) return
+
return (
-
+
{isSuspicious && }
{
- const { t } = useTranslate()
- const { query } = useRouter()
- const { isOpen, onOpen, onClose } = useDisclosure()
-
- useEffect(() => {
- if (query.isFirstBot) onOpen()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- return (
-
-
-
-
-
-
-
- {t('editor.gettingStartedModal.editorBasics.heading')}
-
-
-
-
- 1
-
-
- {t('editor.gettingStartedModal.editorBasics.list.one.label')}
-
-
-
-
- 2
-
-
- {t('editor.gettingStartedModal.editorBasics.list.two.label')}
-
-
-
-
- 3
-
-
- {t(
- 'editor.gettingStartedModal.editorBasics.list.three.label'
- )}
-
-
-
-
- 4
-
-
- {t('editor.gettingStartedModal.editorBasics.list.four.label')}
-
-
-
-
-
- {t('editor.gettingStartedModal.editorBasics.list.label')}
-
-
- {t('editor.gettingStartedModal.seeAction.label')} ({`<`}{' '}
- {t('editor.gettingStartedModal.seeAction.time')})
-
-
-
-
-
-
- {t('editor.gettingStartedModal.seeAction.item.label')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/apps/builder/src/features/editor/components/TypebotHeader.tsx b/apps/builder/src/features/editor/components/TypebotHeader.tsx
index 548c8fb1b..fb6e21623 100644
--- a/apps/builder/src/features/editor/components/TypebotHeader.tsx
+++ b/apps/builder/src/features/editor/components/TypebotHeader.tsx
@@ -60,7 +60,7 @@ export const TypebotHeader = () => {
justify="center"
align="center"
h={`${headerHeight}px`}
- zIndex={100}
+ zIndex={1}
pos="relative"
bgColor={headerBgColor}
flexShrink={0}
diff --git a/apps/builder/src/features/folders/components/CreateBotButton.tsx b/apps/builder/src/features/folders/components/CreateBotButton.tsx
index ee9a05f3b..d0ad6fdca 100644
--- a/apps/builder/src/features/folders/components/CreateBotButton.tsx
+++ b/apps/builder/src/features/folders/components/CreateBotButton.tsx
@@ -8,9 +8,8 @@ import { useTypebotDnd } from '../TypebotDndProvider'
export const CreateBotButton = ({
folderId,
- isFirstBot,
...props
-}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
+}: { folderId?: string } & ButtonProps) => {
const { t } = useTranslate()
const router = useRouter()
const { draggedTypebot } = useTypebotDnd()
@@ -18,7 +17,6 @@ export const CreateBotButton = ({
const handleClick = () =>
router.push(
`/typebots/create?${stringify({
- isFirstBot: !isFirstBot ? undefined : isFirstBot,
folderId,
})}`
)
diff --git a/apps/builder/src/features/folders/components/FolderContent.tsx b/apps/builder/src/features/folders/components/FolderContent.tsx
index 7a67d67bc..6352c1d00 100644
--- a/apps/builder/src/features/folders/components/FolderContent.tsx
+++ b/apps/builder/src/features/folders/components/FolderContent.tsx
@@ -187,7 +187,6 @@ export const FolderContent = ({ folder }: Props) => {
)}
{isFolderLoading && }
diff --git a/apps/builder/src/features/graph/components/nodes/block/SettingsHoverBar.tsx b/apps/builder/src/features/graph/components/nodes/block/SettingsHoverBar.tsx
index 686313c55..bfa4ea1b6 100644
--- a/apps/builder/src/features/graph/components/nodes/block/SettingsHoverBar.tsx
+++ b/apps/builder/src/features/graph/components/nodes/block/SettingsHoverBar.tsx
@@ -8,17 +8,26 @@ import {
} from '@chakra-ui/react'
import { BlockWithOptions } from '@typebot.io/schemas'
import { getHelpDocUrl } from '@/features/graph/helpers/getHelpDocUrl'
-import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
import { useTranslate } from '@tolgee/react'
+import { VideoOnboardingPopover } from '@/features/onboarding/components/VideoOnboardingPopover'
+import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
type Props = {
blockType: BlockWithOptions['type']
+ blockDef?: (typeof forgedBlocks)[keyof typeof forgedBlocks]
+ isVideoOnboardingItemDisplayed: boolean
onExpandClick: () => void
+ onVideoOnboardingClick: () => void
}
-export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
+export const SettingsHoverBar = ({
+ blockType,
+ blockDef,
+ isVideoOnboardingItemDisplayed,
+ onExpandClick,
+ onVideoOnboardingClick,
+}: Props) => {
const { t } = useTranslate()
- const { blockDef } = useForgedBlock(blockType)
const helpDocUrl = getHelpDocUrl(blockType, blockDef)
return (
{
as={Link}
leftIcon={}
borderLeftRadius="none"
+ borderRightRadius={
+ isVideoOnboardingItemDisplayed ? 'none' : undefined
+ }
+ borderRightWidth={isVideoOnboardingItemDisplayed ? '1px' : undefined}
size="xs"
variant="ghost"
href={helpDocUrl}
@@ -51,6 +64,13 @@ export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
{t('help')}
)}
+ {isVideoOnboardingItemDisplayed && (
+
+ )}
)
}
diff --git a/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx b/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx
index f27bf33da..b6922f719 100644
--- a/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx
+++ b/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx
@@ -45,6 +45,9 @@ import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integr
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { ForgedBlockSettings } from '../../../../forge/components/ForgedBlockSettings'
import { OpenAISettings } from '@/features/blocks/integrations/openai/components/OpenAISettings'
+import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
+import { VideoOnboardingPopover } from '@/features/onboarding/components/VideoOnboardingPopover'
+import { hasOnboardingVideo } from '@/features/onboarding/helpers/hasOnboardingVideo'
type Props = {
block: BlockWithOptions
@@ -56,6 +59,7 @@ type Props = {
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
const [isHovering, setIsHovering] = useState(false)
const arrowColor = useColorModeValue('white', 'gray.800')
+ const { blockDef } = useForgedBlock(props.block.type)
const ref = useRef(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
@@ -63,39 +67,54 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
e.stopPropagation()
}
useEventListener('wheel', handleMouseWheel, ref.current)
+
return (
- setIsHovering(true)}
- onMouseLeave={() => setIsHovering(false)}
+
+
-
- (
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ )}
+
)
diff --git a/apps/builder/src/features/auth/components/OnboardingPage.tsx b/apps/builder/src/features/onboarding/components/OnboardingPage.tsx
similarity index 99%
rename from apps/builder/src/features/auth/components/OnboardingPage.tsx
rename to apps/builder/src/features/onboarding/components/OnboardingPage.tsx
index be63e38da..99b27494d 100644
--- a/apps/builder/src/features/auth/components/OnboardingPage.tsx
+++ b/apps/builder/src/features/onboarding/components/OnboardingPage.tsx
@@ -102,7 +102,7 @@ export const OnboardingPage = () => {
setTimeout(() => {
replace({
pathname: '/typebots',
- query: { ...query, isFirstBot: true },
+ query: { ...query },
})
}, 2000)
}
diff --git a/apps/builder/src/features/onboarding/components/VideoOnboardingFloatingWindow.tsx b/apps/builder/src/features/onboarding/components/VideoOnboardingFloatingWindow.tsx
new file mode 100644
index 000000000..119b1fa7f
--- /dev/null
+++ b/apps/builder/src/features/onboarding/components/VideoOnboardingFloatingWindow.tsx
@@ -0,0 +1,68 @@
+import { CloseIcon } from '@/components/icons'
+import { useUser } from '@/features/account/hooks/useUser'
+import {
+ IconButton,
+ SlideFade,
+ Flex,
+ useColorModeValue,
+} from '@chakra-ui/react'
+import { useOnboardingDisclosure } from '../hooks/useOnboardingDisclosure'
+import { onboardingVideos } from '../data'
+import { YoutubeIframe } from './YoutubeIframe'
+
+type Props = {
+ type: keyof typeof onboardingVideos
+}
+
+export const VideoOnboardingFloatingWindow = ({ type }: Props) => {
+ const { user, updateUser } = useUser()
+ const { isOpen, onClose } = useOnboardingDisclosure({
+ key: type,
+ user,
+ updateUser,
+ defaultOpenDelay: 1000,
+ blockDef: undefined,
+ })
+ const bgColor = useColorModeValue('white', 'gray.900')
+ const closeButtonColorScheme = useColorModeValue('blackAlpha', 'gray')
+
+ if (!onboardingVideos[type]) return null
+
+ return (
+
+
+
+
+ }
+ aria-label={'Close'}
+ pos="absolute"
+ top="-3"
+ right="-3"
+ colorScheme={closeButtonColorScheme}
+ size="sm"
+ rounded="full"
+ onClick={onClose}
+ />
+
+
+ )
+}
diff --git a/apps/builder/src/features/onboarding/components/VideoOnboardingPopover.tsx b/apps/builder/src/features/onboarding/components/VideoOnboardingPopover.tsx
new file mode 100644
index 000000000..ec0de7886
--- /dev/null
+++ b/apps/builder/src/features/onboarding/components/VideoOnboardingPopover.tsx
@@ -0,0 +1,76 @@
+import { CloseIcon, VideoPopoverIcon } from '@/components/icons'
+import {
+ PopoverContent,
+ PopoverArrow,
+ PopoverBody,
+ IconButton,
+ Popover,
+ PopoverTrigger,
+ IconButtonProps,
+} from '@chakra-ui/react'
+import { useOnboardingDisclosure } from '../hooks/useOnboardingDisclosure'
+import { onboardingVideos } from '../data'
+import { useUser } from '@/features/account/hooks/useUser'
+import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
+import { YoutubeIframe } from './YoutubeIframe'
+
+type Props = {
+ type: keyof typeof onboardingVideos
+ defaultIsOpen?: boolean
+ blockDef?: ForgedBlockDefinition
+ children: ({ onToggle }: { onToggle: () => void }) => JSX.Element
+}
+
+const Root = ({ type, blockDef, children }: Props) => {
+ const { user, updateUser } = useUser()
+ const youtubeId =
+ onboardingVideos[type]?.youtubeId ?? blockDef?.onboarding?.youtubeId
+ const { isOpen, onClose, onToggle } = useOnboardingDisclosure({
+ key: type,
+ updateUser,
+ user,
+ blockDef,
+ })
+
+ if (!youtubeId) return children({ onToggle })
+
+ return (
+
+ {children({ onToggle })}
+
+
+
+
+
+ }
+ aria-label={'Close'}
+ pos="absolute"
+ top="-3"
+ right="-3"
+ colorScheme="blackAlpha"
+ size="sm"
+ rounded="full"
+ onClick={onClose}
+ />
+
+
+
+ )
+}
+
+const TriggerIconButton = (props: Omit) => (
+ }
+ aria-label={'Open Bubbles help video'}
+ variant="ghost"
+ colorScheme="blue"
+ {...props}
+ />
+)
+
+export const VideoOnboardingPopover = {
+ Root,
+ TriggerIconButton,
+}
diff --git a/apps/builder/src/features/onboarding/components/YoutubeIframe.tsx b/apps/builder/src/features/onboarding/components/YoutubeIframe.tsx
new file mode 100644
index 000000000..463ec3090
--- /dev/null
+++ b/apps/builder/src/features/onboarding/components/YoutubeIframe.tsx
@@ -0,0 +1,60 @@
+import { chakra } from '@chakra-ui/react'
+import { useCallback, useEffect } from 'react'
+
+declare const window: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ YT:
+ | undefined
+ | {
+ Player: new (
+ id: string,
+ options: { events: { onReady: () => void } }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ) => any
+ loaded: boolean
+ }
+ onYouTubeIframeAPIReady: () => void
+}
+
+type Props = {
+ id: string
+}
+
+export const YoutubeIframe = ({ id }: Props) => {
+ const initPlayer = useCallback(() => {
+ if (!window.YT) return
+ const player = new window.YT.Player(id, {
+ events: {
+ onReady: () => {
+ player.setPlaybackRate(1.2)
+ player.setPlaybackQuality('')
+ },
+ },
+ })
+ }, [id])
+
+ useEffect(() => {
+ if (window.YT?.loaded) initPlayer()
+ initYoutubeIframeApi()
+ window.onYouTubeIframeAPIReady = initPlayer
+ }, [initPlayer])
+
+ return (
+
+ )
+}
+
+const initYoutubeIframeApi = () => {
+ if (document.getElementById('youtube-iframe-api')) return
+ const tag = document.createElement('script')
+ tag.src = 'https://www.youtube.com/iframe_api'
+ tag.id = 'youtube-iframe-api'
+ document.head.appendChild(tag)
+}
diff --git a/apps/builder/src/features/onboarding/data.ts b/apps/builder/src/features/onboarding/data.ts
new file mode 100644
index 000000000..80cecbc5e
--- /dev/null
+++ b/apps/builder/src/features/onboarding/data.ts
@@ -0,0 +1,32 @@
+import { Block } from '@typebot.io/schemas'
+import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
+
+type Feature = 'editor' | Block['type']
+
+export const onboardingVideos: Partial<
+ Record<
+ Feature,
+ | {
+ key: string
+ youtubeId: string
+ deployedAt: Date
+ }
+ | undefined
+ >
+> = {
+ editor: {
+ key: 'editor',
+ youtubeId: 'jp3ggg_42-M',
+ deployedAt: new Date('2024-06-04'),
+ },
+ [IntegrationBlockType.ZAPIER]: {
+ key: IntegrationBlockType.ZAPIER,
+ youtubeId: '2ZskGItI_Zo',
+ deployedAt: new Date('2024-06-04'),
+ },
+ [IntegrationBlockType.MAKE_COM]: {
+ key: IntegrationBlockType.MAKE_COM,
+ youtubeId: 'V-y1Orys_kY',
+ deployedAt: new Date('2024-06-04'),
+ },
+}
diff --git a/apps/builder/src/features/onboarding/helpers/hasOnboardingVideo.ts b/apps/builder/src/features/onboarding/helpers/hasOnboardingVideo.ts
new file mode 100644
index 000000000..72edbe5d0
--- /dev/null
+++ b/apps/builder/src/features/onboarding/helpers/hasOnboardingVideo.ts
@@ -0,0 +1,13 @@
+import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
+import { Block } from '@typebot.io/schemas'
+import { onboardingVideos } from '../data'
+import { isDefined } from '@typebot.io/lib/utils'
+
+type Props = {
+ blockType: Block['type']
+ blockDef?: ForgedBlockDefinition
+}
+export const hasOnboardingVideo = ({ blockType, blockDef }: Props) =>
+ isDefined(
+ blockDef?.onboarding?.youtubeId ?? onboardingVideos[blockType]?.youtubeId
+ )
diff --git a/apps/builder/src/features/onboarding/hooks/useOnboardingDisclosure.ts b/apps/builder/src/features/onboarding/hooks/useOnboardingDisclosure.ts
new file mode 100644
index 000000000..45fd6a63e
--- /dev/null
+++ b/apps/builder/src/features/onboarding/hooks/useOnboardingDisclosure.ts
@@ -0,0 +1,65 @@
+import { useDisclosure } from '@chakra-ui/react'
+import { onboardingVideos } from '../data'
+import { User } from '@typebot.io/schemas'
+import { useEffect, useState } from 'react'
+import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
+
+type Props = {
+ key?: keyof typeof onboardingVideos
+ updateUser: (data: Partial) => void
+ user?: Pick
+ defaultOpenDelay?: number
+ blockDef: ForgedBlockDefinition | undefined
+}
+
+export const useOnboardingDisclosure = ({
+ key,
+ updateUser,
+ user,
+ defaultOpenDelay,
+ blockDef,
+}: Props) => {
+ const [isInitialized, setIsInitialized] = useState(false)
+ const { isOpen, onOpen, onClose, onToggle } = useDisclosure({
+ onOpen: () => {
+ if (!user || !key || user.displayedInAppNotifications?.[key]) return
+ updateUser({
+ displayedInAppNotifications: {
+ ...user.displayedInAppNotifications,
+ [key]: true,
+ },
+ })
+ },
+ })
+
+ useEffect(() => {
+ if (isInitialized || !user?.createdAt || !key) return
+ setIsInitialized(true)
+ if (
+ key &&
+ new Date(user.createdAt) >=
+ (onboardingVideos[key]
+ ? onboardingVideos[key]!.deployedAt
+ : blockDef?.onboarding?.deployedAt ?? new Date()) &&
+ user.displayedInAppNotifications?.[key] === undefined
+ ) {
+ if (defaultOpenDelay) {
+ setTimeout(() => {
+ onOpen()
+ }, defaultOpenDelay)
+ } else {
+ onOpen()
+ }
+ }
+ }, [
+ blockDef?.onboarding?.deployedAt,
+ defaultOpenDelay,
+ isInitialized,
+ key,
+ onOpen,
+ user?.createdAt,
+ user?.displayedInAppNotifications,
+ ])
+
+ return { isOpen, onClose, onToggle }
+}
diff --git a/apps/builder/src/features/templates/components/CreateNewTypebotButtons.tsx b/apps/builder/src/features/templates/components/CreateNewTypebotButtons.tsx
index 1c4ca3c9f..56fa1cf4f 100644
--- a/apps/builder/src/features/templates/components/CreateNewTypebotButtons.tsx
+++ b/apps/builder/src/features/templates/components/CreateNewTypebotButtons.tsx
@@ -42,12 +42,6 @@ export const CreateNewTypebotButtons = () => {
onSuccess: (data) => {
router.push({
pathname: `/typebots/${data.typebot.id}/edit`,
- query:
- router.query.isFirstBot === 'true'
- ? {
- isFirstBot: 'true',
- }
- : {},
})
},
onSettled: () => {
@@ -68,12 +62,6 @@ export const CreateNewTypebotButtons = () => {
onSuccess: (data) => {
router.push({
pathname: `/typebots/${data.typebot.id}/edit`,
- query:
- router.query.isFirstBot === 'true'
- ? {
- isFirstBot: 'true',
- }
- : {},
})
},
onSettled: () => {
diff --git a/apps/builder/src/pages/onboarding.tsx b/apps/builder/src/pages/onboarding.tsx
index 080b1ce5d..99041a49d 100644
--- a/apps/builder/src/pages/onboarding.tsx
+++ b/apps/builder/src/pages/onboarding.tsx
@@ -1,4 +1,4 @@
-import { OnboardingPage } from '@/features/auth/components/OnboardingPage'
+import { OnboardingPage } from '@/features/onboarding/components/OnboardingPage'
export default function Page() {
return
diff --git a/packages/forge/core/types.ts b/packages/forge/core/types.ts
index 325a94fcd..1a7b88056 100644
--- a/packages/forge/core/types.ts
+++ b/packages/forge/core/types.ts
@@ -136,6 +136,10 @@ export type BlockDefinition<
LightLogo: (props: SVGProps) => JSX.Element
DarkLogo?: (props: SVGProps) => JSX.Element
docsUrl?: string
+ onboarding?: {
+ deployedAt: Date
+ youtubeId: string
+ }
auth?: Auth
options?: Options | undefined
fetchers?: FetcherDefinition[]