🚸 (onboarding) Introduce new onboarding floating videos mechanism
This commit is contained in:
@ -678,3 +678,20 @@ export const BracesIcon = (props: IconProps) => (
|
|||||||
<path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" />
|
<path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const VideoPopoverIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 21 21" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path
|
||||||
|
d="M6.9 18.0079C8.80858 18.9869 11.0041 19.2521 13.0909 18.7556C15.1777 18.2592 17.0186 17.0337 18.2818 15.3C19.545 13.5664 20.1474 11.4386 19.9806 9.30002C19.8137 7.16147 18.8886 5.15283 17.3718 3.63605C15.855 2.11928 13.8464 1.19411 11.7078 1.02728C9.56929 0.860441 7.44147 1.46291 5.70782 2.72611C3.97417 3.98931 2.74869 5.83017 2.25222 7.91697C1.75575 10.0038 2.02094 12.1993 3 14.1079L1 20.0079L6.9 18.0079Z"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9 6L15 10L9 14V6Z"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
@ -18,27 +18,19 @@ import {
|
|||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons'
|
||||||
import { useTypebot } from '../providers/TypebotProvider'
|
import { useTypebot } from '../providers/TypebotProvider'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { EditorSettingsModal } from './EditorSettingsModal'
|
import { EditorSettingsModal } from './EditorSettingsModal'
|
||||||
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
|
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { useUser } from '@/features/account/hooks/useUser'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
||||||
|
|
||||||
export const BoardMenuButton = (props: StackProps) => {
|
export const BoardMenuButton = (props: StackProps) => {
|
||||||
const { query } = useRouter()
|
|
||||||
const { typebot, currentUserMode } = useTypebot()
|
const { typebot, currentUserMode } = useTypebot()
|
||||||
const { user } = useUser()
|
|
||||||
const [isDownloading, setIsDownloading] = useState(false)
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const { setRightPanel } = useEditor()
|
const { setRightPanel } = useEditor()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user && !user.graphNavigation && !query.isFirstBot) onOpen()
|
|
||||||
}, [onOpen, query.isFirstBot, user, user?.graphNavigation])
|
|
||||||
|
|
||||||
const downloadFlow = () => {
|
const downloadFlow = () => {
|
||||||
assert(typebot)
|
assert(typebot)
|
||||||
setIsDownloading(true)
|
setIsDownloading(true)
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
import { useTypebot } from '../providers/TypebotProvider'
|
import { useTypebot } from '../providers/TypebotProvider'
|
||||||
import { BlocksSideBar } from './BlocksSideBar'
|
import { BlocksSideBar } from './BlocksSideBar'
|
||||||
import { BoardMenuButton } from './BoardMenuButton'
|
import { BoardMenuButton } from './BoardMenuButton'
|
||||||
import { GettingStartedModal } from './GettingStartedModal'
|
|
||||||
import { PreviewDrawer } from '@/features/preview/components/PreviewDrawer'
|
import { PreviewDrawer } from '@/features/preview/components/PreviewDrawer'
|
||||||
import { TypebotHeader } from './TypebotHeader'
|
import { TypebotHeader } from './TypebotHeader'
|
||||||
import { Graph } from '@/features/graph/components/Graph'
|
import { Graph } from '@/features/graph/components/Graph'
|
||||||
@ -19,6 +18,7 @@ import { TypebotNotFoundPage } from './TypebotNotFoundPage'
|
|||||||
import { SuspectedTypebotBanner } from './SuspectedTypebotBanner'
|
import { SuspectedTypebotBanner } from './SuspectedTypebotBanner'
|
||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { VariablesDrawer } from '@/features/preview/components/VariablesDrawer'
|
import { VariablesDrawer } from '@/features/preview/components/VariablesDrawer'
|
||||||
|
import { VideoOnboardingFloatingWindow } from '@/features/onboarding/components/VideoOnboardingFloatingWindow'
|
||||||
|
|
||||||
export const EditorPage = () => {
|
export const EditorPage = () => {
|
||||||
const { typebot, currentUserMode, is404 } = useTypebot()
|
const { typebot, currentUserMode, is404 } = useTypebot()
|
||||||
@ -32,11 +32,12 @@ export const EditorPage = () => {
|
|||||||
const isSuspicious = typebot?.riskLevel === 100 && !workspace?.isVerified
|
const isSuspicious = typebot?.riskLevel === 100 && !workspace?.isVerified
|
||||||
|
|
||||||
if (is404) return <TypebotNotFoundPage />
|
if (is404) return <TypebotNotFoundPage />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorProvider>
|
<EditorProvider>
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Editor` : 'Editor'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Editor` : 'Editor'} />
|
||||||
<Flex overflow="clip" h="100vh" flexDir="column" id="editor-container">
|
<Flex overflow="clip" h="100vh" flexDir="column" id="editor-container">
|
||||||
<GettingStartedModal />
|
<VideoOnboardingFloatingWindow type="editor" />
|
||||||
{isSuspicious && <SuspectedTypebotBanner typebotId={typebot.id} />}
|
{isSuspicious && <SuspectedTypebotBanner typebotId={typebot.id} />}
|
||||||
<TypebotHeader />
|
<TypebotHeader />
|
||||||
<Flex
|
<Flex
|
||||||
|
@ -1,174 +0,0 @@
|
|||||||
import {
|
|
||||||
useDisclosure,
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
|
||||||
Stack,
|
|
||||||
Heading,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
Text,
|
|
||||||
Accordion,
|
|
||||||
AccordionButton,
|
|
||||||
AccordionIcon,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionPanel,
|
|
||||||
Box,
|
|
||||||
HStack,
|
|
||||||
Flex,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useTranslate } from '@tolgee/react'
|
|
||||||
|
|
||||||
export const GettingStartedModal = () => {
|
|
||||||
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 (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody as={Stack} spacing="8" py="10">
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Heading fontSize="xl">
|
|
||||||
{t('editor.gettingStartedModal.editorBasics.heading')}
|
|
||||||
</Heading>
|
|
||||||
<List spacing={4}>
|
|
||||||
<HStack as={ListItem}>
|
|
||||||
<Flex
|
|
||||||
bgColor="blue.500"
|
|
||||||
rounded="full"
|
|
||||||
boxSize="25px"
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
color="white"
|
|
||||||
fontWeight="bold"
|
|
||||||
flexShrink={0}
|
|
||||||
fontSize="13px"
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</Flex>
|
|
||||||
<Text>
|
|
||||||
{t('editor.gettingStartedModal.editorBasics.list.one.label')}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack as={ListItem}>
|
|
||||||
<Flex
|
|
||||||
bgColor="blue.500"
|
|
||||||
rounded="full"
|
|
||||||
boxSize="25px"
|
|
||||||
fontSize="13px"
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
color="white"
|
|
||||||
fontWeight="bold"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
2
|
|
||||||
</Flex>
|
|
||||||
<Text>
|
|
||||||
{t('editor.gettingStartedModal.editorBasics.list.two.label')}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack as={ListItem}>
|
|
||||||
<Flex
|
|
||||||
bgColor="blue.500"
|
|
||||||
rounded="full"
|
|
||||||
boxSize="25px"
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
color="white"
|
|
||||||
fontWeight="bold"
|
|
||||||
flexShrink={0}
|
|
||||||
fontSize="13px"
|
|
||||||
>
|
|
||||||
3
|
|
||||||
</Flex>
|
|
||||||
<Text>
|
|
||||||
{t(
|
|
||||||
'editor.gettingStartedModal.editorBasics.list.three.label'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack as={ListItem}>
|
|
||||||
<Flex
|
|
||||||
bgColor="blue.500"
|
|
||||||
rounded="full"
|
|
||||||
boxSize="25px"
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
color="white"
|
|
||||||
fontWeight="bold"
|
|
||||||
flexShrink={0}
|
|
||||||
fontSize="13px"
|
|
||||||
>
|
|
||||||
4
|
|
||||||
</Flex>
|
|
||||||
<Text>
|
|
||||||
{t('editor.gettingStartedModal.editorBasics.list.four.label')}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</List>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text>{t('editor.gettingStartedModal.editorBasics.list.label')}</Text>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Heading fontSize="xl">
|
|
||||||
{t('editor.gettingStartedModal.seeAction.label')} ({`<`}{' '}
|
|
||||||
{t('editor.gettingStartedModal.seeAction.time')})
|
|
||||||
</Heading>
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="315"
|
|
||||||
src="https://www.youtube.com/embed/jp3ggg_42-M"
|
|
||||||
title="YouTube video player"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
style={{ borderRadius: '0.5rem', border: 'none' }}
|
|
||||||
/>
|
|
||||||
<Accordion allowToggle>
|
|
||||||
<AccordionItem>
|
|
||||||
<AccordionButton>
|
|
||||||
<Box flex="1" textAlign="left">
|
|
||||||
{t('editor.gettingStartedModal.seeAction.item.label')}
|
|
||||||
</Box>
|
|
||||||
<AccordionIcon />
|
|
||||||
</AccordionButton>
|
|
||||||
<AccordionPanel py={10} as={Stack} spacing="10">
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="315"
|
|
||||||
src="https://www.youtube.com/embed/6BudIC4GYNk"
|
|
||||||
title="YouTube video player"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
style={{ borderRadius: '0.5rem', border: 'none' }}
|
|
||||||
/>
|
|
||||||
<iframe
|
|
||||||
width="100%"
|
|
||||||
height="315"
|
|
||||||
src="https://www.youtube.com/embed/ZuyDwFLRbfQ"
|
|
||||||
title="YouTube video player"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
style={{ borderRadius: '0.5rem', border: 'none' }}
|
|
||||||
/>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</Stack>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
@ -60,7 +60,7 @@ export const TypebotHeader = () => {
|
|||||||
justify="center"
|
justify="center"
|
||||||
align="center"
|
align="center"
|
||||||
h={`${headerHeight}px`}
|
h={`${headerHeight}px`}
|
||||||
zIndex={100}
|
zIndex={1}
|
||||||
pos="relative"
|
pos="relative"
|
||||||
bgColor={headerBgColor}
|
bgColor={headerBgColor}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
@ -8,9 +8,8 @@ import { useTypebotDnd } from '../TypebotDndProvider'
|
|||||||
|
|
||||||
export const CreateBotButton = ({
|
export const CreateBotButton = ({
|
||||||
folderId,
|
folderId,
|
||||||
isFirstBot,
|
|
||||||
...props
|
...props
|
||||||
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
|
}: { folderId?: string } & ButtonProps) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { draggedTypebot } = useTypebotDnd()
|
const { draggedTypebot } = useTypebotDnd()
|
||||||
@ -18,7 +17,6 @@ export const CreateBotButton = ({
|
|||||||
const handleClick = () =>
|
const handleClick = () =>
|
||||||
router.push(
|
router.push(
|
||||||
`/typebots/create?${stringify({
|
`/typebots/create?${stringify({
|
||||||
isFirstBot: !isFirstBot ? undefined : isFirstBot,
|
|
||||||
folderId,
|
folderId,
|
||||||
})}`
|
})}`
|
||||||
)
|
)
|
||||||
|
@ -187,7 +187,6 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
<CreateBotButton
|
<CreateBotButton
|
||||||
folderId={folder?.id}
|
folderId={folder?.id}
|
||||||
isLoading={isTypebotLoading}
|
isLoading={isTypebotLoading}
|
||||||
isFirstBot={typebots?.length === 0 && folder === null}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isFolderLoading && <ButtonSkeleton />}
|
{isFolderLoading && <ButtonSkeleton />}
|
||||||
|
@ -8,17 +8,26 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { BlockWithOptions } from '@typebot.io/schemas'
|
import { BlockWithOptions } from '@typebot.io/schemas'
|
||||||
import { getHelpDocUrl } from '@/features/graph/helpers/getHelpDocUrl'
|
import { getHelpDocUrl } from '@/features/graph/helpers/getHelpDocUrl'
|
||||||
import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
|
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { VideoOnboardingPopover } from '@/features/onboarding/components/VideoOnboardingPopover'
|
||||||
|
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
blockType: BlockWithOptions['type']
|
blockType: BlockWithOptions['type']
|
||||||
|
blockDef?: (typeof forgedBlocks)[keyof typeof forgedBlocks]
|
||||||
|
isVideoOnboardingItemDisplayed: boolean
|
||||||
onExpandClick: () => void
|
onExpandClick: () => void
|
||||||
|
onVideoOnboardingClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
export const SettingsHoverBar = ({
|
||||||
|
blockType,
|
||||||
|
blockDef,
|
||||||
|
isVideoOnboardingItemDisplayed,
|
||||||
|
onExpandClick,
|
||||||
|
onVideoOnboardingClick,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const { blockDef } = useForgedBlock(blockType)
|
|
||||||
const helpDocUrl = getHelpDocUrl(blockType, blockDef)
|
const helpDocUrl = getHelpDocUrl(blockType, blockDef)
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@ -43,6 +52,10 @@ export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
|||||||
as={Link}
|
as={Link}
|
||||||
leftIcon={<BuoyIcon />}
|
leftIcon={<BuoyIcon />}
|
||||||
borderLeftRadius="none"
|
borderLeftRadius="none"
|
||||||
|
borderRightRadius={
|
||||||
|
isVideoOnboardingItemDisplayed ? 'none' : undefined
|
||||||
|
}
|
||||||
|
borderRightWidth={isVideoOnboardingItemDisplayed ? '1px' : undefined}
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
href={helpDocUrl}
|
href={helpDocUrl}
|
||||||
@ -51,6 +64,13 @@ export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
|||||||
{t('help')}
|
{t('help')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isVideoOnboardingItemDisplayed && (
|
||||||
|
<VideoOnboardingPopover.TriggerIconButton
|
||||||
|
onClick={onVideoOnboardingClick}
|
||||||
|
size="xs"
|
||||||
|
borderLeftRadius="none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,9 @@ import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integr
|
|||||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||||
import { ForgedBlockSettings } from '../../../../forge/components/ForgedBlockSettings'
|
import { ForgedBlockSettings } from '../../../../forge/components/ForgedBlockSettings'
|
||||||
import { OpenAISettings } from '@/features/blocks/integrations/openai/components/OpenAISettings'
|
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 = {
|
type Props = {
|
||||||
block: BlockWithOptions
|
block: BlockWithOptions
|
||||||
@ -56,6 +59,7 @@ type Props = {
|
|||||||
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||||
const [isHovering, setIsHovering] = useState(false)
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
const arrowColor = useColorModeValue('white', 'gray.800')
|
const arrowColor = useColorModeValue('white', 'gray.800')
|
||||||
|
const { blockDef } = useForgedBlock(props.block.type)
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||||
|
|
||||||
@ -63,39 +67,54 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
useEventListener('wheel', handleMouseWheel, ref.current)
|
useEventListener('wheel', handleMouseWheel, ref.current)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||||
<PopoverArrow bgColor={arrowColor} />
|
<PopoverArrow bgColor={arrowColor} />
|
||||||
<PopoverBody
|
|
||||||
py="3"
|
<VideoOnboardingPopover.Root
|
||||||
overflowY="auto"
|
type={props.block.type}
|
||||||
maxH="400px"
|
blockDef={blockDef}
|
||||||
ref={ref}
|
|
||||||
shadow="lg"
|
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
|
||||||
>
|
>
|
||||||
<Stack spacing={3}>
|
{({ onToggle }) => (
|
||||||
<Flex
|
<PopoverBody
|
||||||
w="full"
|
py="3"
|
||||||
pos="absolute"
|
overflowY="auto"
|
||||||
top="-56px"
|
maxH="400px"
|
||||||
height="64px"
|
ref={ref}
|
||||||
right={0}
|
shadow="lg"
|
||||||
justifyContent="flex-end"
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
align="center"
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
>
|
>
|
||||||
<SlideFade in={isHovering} unmountOnExit>
|
<Stack spacing={3}>
|
||||||
<SettingsHoverBar
|
<Flex
|
||||||
onExpandClick={onExpandClick}
|
w="full"
|
||||||
blockType={props.block.type}
|
pos="absolute"
|
||||||
/>
|
top="-56px"
|
||||||
</SlideFade>
|
height="64px"
|
||||||
</Flex>
|
right={0}
|
||||||
<BlockSettings {...props} />
|
justifyContent="flex-end"
|
||||||
</Stack>
|
align="center"
|
||||||
</PopoverBody>
|
>
|
||||||
|
<SlideFade in={isHovering} unmountOnExit>
|
||||||
|
<SettingsHoverBar
|
||||||
|
onExpandClick={onExpandClick}
|
||||||
|
onVideoOnboardingClick={onToggle}
|
||||||
|
blockType={props.block.type}
|
||||||
|
blockDef={blockDef}
|
||||||
|
isVideoOnboardingItemDisplayed={hasOnboardingVideo({
|
||||||
|
blockType: props.block.type,
|
||||||
|
blockDef,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</SlideFade>
|
||||||
|
</Flex>
|
||||||
|
<BlockSettings {...props} />
|
||||||
|
</Stack>
|
||||||
|
</PopoverBody>
|
||||||
|
)}
|
||||||
|
</VideoOnboardingPopover.Root>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
|
@ -102,7 +102,7 @@ export const OnboardingPage = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
replace({
|
replace({
|
||||||
pathname: '/typebots',
|
pathname: '/typebots',
|
||||||
query: { ...query, isFirstBot: true },
|
query: { ...query },
|
||||||
})
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
@ -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 (
|
||||||
|
<SlideFade
|
||||||
|
in={isOpen}
|
||||||
|
offsetY="20px"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '18px',
|
||||||
|
right: '18px',
|
||||||
|
zIndex: 42,
|
||||||
|
}}
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
p="5"
|
||||||
|
bgColor={bgColor}
|
||||||
|
borderWidth="1px"
|
||||||
|
shadow="xl"
|
||||||
|
rounded="md"
|
||||||
|
aspectRatio="1.5"
|
||||||
|
w="600px"
|
||||||
|
>
|
||||||
|
<YoutubeIframe id={onboardingVideos[type]!.youtubeId} />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
aria-label={'Close'}
|
||||||
|
pos="absolute"
|
||||||
|
top="-3"
|
||||||
|
right="-3"
|
||||||
|
colorScheme={closeButtonColorScheme}
|
||||||
|
size="sm"
|
||||||
|
rounded="full"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</SlideFade>
|
||||||
|
)
|
||||||
|
}
|
@ -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 (
|
||||||
|
<Popover isOpen={isOpen} placement="right" isLazy>
|
||||||
|
<PopoverTrigger>{children({ onToggle })}</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent aspectRatio="1.5" width="640px">
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody h="full" p="5">
|
||||||
|
<YoutubeIframe id={youtubeId} />
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
aria-label={'Close'}
|
||||||
|
pos="absolute"
|
||||||
|
top="-3"
|
||||||
|
right="-3"
|
||||||
|
colorScheme="blackAlpha"
|
||||||
|
size="sm"
|
||||||
|
rounded="full"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TriggerIconButton = (props: Omit<IconButtonProps, 'aria-label'>) => (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={<VideoPopoverIcon />}
|
||||||
|
aria-label={'Open Bubbles help video'}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="blue"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const VideoOnboardingPopover = {
|
||||||
|
Root,
|
||||||
|
TriggerIconButton,
|
||||||
|
}
|
@ -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 (
|
||||||
|
<chakra.iframe
|
||||||
|
id={id}
|
||||||
|
src={`https://www.youtube.com/embed/${id}?autoplay=1&enablejsapi=1`}
|
||||||
|
allowFullScreen
|
||||||
|
allow="autoplay; fullscreen; picture-in-picture"
|
||||||
|
boxSize="full"
|
||||||
|
rounded="md"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
32
apps/builder/src/features/onboarding/data.ts
Normal file
32
apps/builder/src/features/onboarding/data.ts
Normal file
@ -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'),
|
||||||
|
},
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -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<User>) => void
|
||||||
|
user?: Pick<User, 'createdAt' | 'displayedInAppNotifications'>
|
||||||
|
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 }
|
||||||
|
}
|
@ -42,12 +42,6 @@ export const CreateNewTypebotButtons = () => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/typebots/${data.typebot.id}/edit`,
|
pathname: `/typebots/${data.typebot.id}/edit`,
|
||||||
query:
|
|
||||||
router.query.isFirstBot === 'true'
|
|
||||||
? {
|
|
||||||
isFirstBot: 'true',
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@ -68,12 +62,6 @@ export const CreateNewTypebotButtons = () => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/typebots/${data.typebot.id}/edit`,
|
pathname: `/typebots/${data.typebot.id}/edit`,
|
||||||
query:
|
|
||||||
router.query.isFirstBot === 'true'
|
|
||||||
? {
|
|
||||||
isFirstBot: 'true',
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { OnboardingPage } from '@/features/auth/components/OnboardingPage'
|
import { OnboardingPage } from '@/features/onboarding/components/OnboardingPage'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <OnboardingPage />
|
return <OnboardingPage />
|
||||||
|
@ -136,6 +136,10 @@ export type BlockDefinition<
|
|||||||
LightLogo: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
LightLogo: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||||
DarkLogo?: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
DarkLogo?: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||||
docsUrl?: string
|
docsUrl?: string
|
||||||
|
onboarding?: {
|
||||||
|
deployedAt: Date
|
||||||
|
youtubeId: string
|
||||||
|
}
|
||||||
auth?: Auth
|
auth?: Auth
|
||||||
options?: Options | undefined
|
options?: Options | undefined
|
||||||
fetchers?: FetcherDefinition<Auth, Options>[]
|
fetchers?: FetcherDefinition<Auth, Options>[]
|
||||||
|
Reference in New Issue
Block a user