♻️ Re-organize workspace folders
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { AudioBubbleContent } from '@typebot.io/schemas'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
|
||||
type Props = {
|
||||
url: AudioBubbleContent['url']
|
||||
onTransitionEnd: () => void
|
||||
}
|
||||
|
||||
const showAnimationDuration = 400
|
||||
const typingDuration = 500
|
||||
|
||||
export const AudioBubble = ({ url, onTransitionEnd }: Props) => {
|
||||
const { typebot, isLoading } = useTypebot()
|
||||
const audio = useRef<HTMLAudioElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
const [parsedUrl] = useState(parseVariables(typebot.variables)(url))
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
|
||||
const typingTimeout = setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}, typingDuration)
|
||||
|
||||
return () => {
|
||||
clearTimeout(typingTimeout)
|
||||
}
|
||||
}, [isLoading, isTyping, onTransitionEnd])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||
<div className={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||
<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 ? <TypingBubble /> : null}
|
||||
</div>
|
||||
<audio
|
||||
ref={audio}
|
||||
src={parsedUrl}
|
||||
className={
|
||||
'z-10 content-opacity m-2 ' +
|
||||
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||
}
|
||||
style={{ height: isTyping ? '2rem' : 'revert' }}
|
||||
autoPlay
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AudioBubble'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './components'
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { EmbedBubbleBlock } from '@typebot.io/schemas'
|
||||
import { TypingBubble } from '../../../../../components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
|
||||
type Props = {
|
||||
block: EmbedBubbleBlock
|
||||
onTransitionEnd: () => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
|
||||
export const EmbedBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
const { typebot, isLoading } = useTypebot()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
const [url] = useState(parseVariables(typebot.variables)(block.content?.url))
|
||||
|
||||
const onTypingEnd = useCallback(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}, [onTransitionEnd])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
const timeout = setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
onTypingEnd()
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [isLoading, isTyping, onTypingEnd])
|
||||
|
||||
const height = block.content.height
|
||||
? typeof block.content.height === 'string'
|
||||
? parseVariables(typebot.variables)(block.content.height) + 'px'
|
||||
: block.content.height
|
||||
: '2rem'
|
||||
|
||||
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 ? <TypingBubble /> : <></>}
|
||||
</div>
|
||||
<iframe
|
||||
id="embed-bubble-content"
|
||||
src={url}
|
||||
className={
|
||||
'w-full z-20 p-4 content-opacity ' +
|
||||
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||
}
|
||||
style={{
|
||||
height: isTyping ? '2rem' : height,
|
||||
borderRadius: '15px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { EmbedBubble } from './components/EmbedBubble'
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { ImageBubbleBlock } from '@typebot.io/schemas'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
|
||||
type Props = {
|
||||
block: ImageBubbleBlock
|
||||
onTransitionEnd: () => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
|
||||
export const mediaLoadingFallbackTimeout = 5000
|
||||
|
||||
export const ImageBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
const { typebot, isLoading } = useTypebot()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const image = useRef<HTMLImageElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
const [url] = useState(parseVariables(typebot.variables)(block.content?.url))
|
||||
|
||||
const onTypingEnd = useCallback(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}, [onTransitionEnd])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
onTypingEnd()
|
||||
}, mediaLoadingFallbackTimeout)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [isLoading, isTyping, onTypingEnd])
|
||||
|
||||
useEffect(() => {
|
||||
const currentImage = image.current
|
||||
if (!currentImage || isLoading || !isTyping) return
|
||||
currentImage.onload = () => {
|
||||
setIsTyping(false)
|
||||
onTypingEnd()
|
||||
}
|
||||
return () => {
|
||||
currentImage.onload = null
|
||||
}
|
||||
}, [isLoading, isTyping, onTypingEnd])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" 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'}>
|
||||
<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 ? <TypingBubble /> : null}
|
||||
</div>
|
||||
<img
|
||||
ref={image}
|
||||
src={url}
|
||||
className={
|
||||
'p-4 content-opacity z-10 w-auto rounded-lg ' +
|
||||
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||
}
|
||||
style={{
|
||||
maxHeight: '32rem',
|
||||
height: isTyping ? '2rem' : 'auto',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
alt="Bubble image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ImageBubble } from './components/ImageBubble'
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { BubbleBlockType, TextBubbleBlock } from '@typebot.io/schemas'
|
||||
import { computeTypingDuration } from '../utils/computeTypingDuration'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
|
||||
type Props = {
|
||||
block: TextBubbleBlock
|
||||
onTransitionEnd: () => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
|
||||
const defaultTypingEmulation = {
|
||||
enabled: true,
|
||||
speed: 300,
|
||||
maxDelay: 1.5,
|
||||
}
|
||||
|
||||
export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
const { typebot, isLoading } = useTypebot()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const [content] = useState(
|
||||
parseVariables(typebot.variables)(block.content.html)
|
||||
)
|
||||
|
||||
const onTypingEnd = useCallback(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}, [onTransitionEnd])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
|
||||
const typingTimeout = computeTypingDuration(
|
||||
block.content.plainText,
|
||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||
)
|
||||
const timeout = setTimeout(() => {
|
||||
onTypingEnd()
|
||||
}, typingTimeout)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [
|
||||
block.content.plainText,
|
||||
isLoading,
|
||||
isTyping,
|
||||
onTypingEnd,
|
||||
typebot.settings?.typingEmulation,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" ref={messageContainer}>
|
||||
<div className="flex mb-2 w-full items-center">
|
||||
<div className={'flex relative items-start typebot-host-bubble'}>
|
||||
<div
|
||||
className="flex items-center absolute px-4 py-2 rounded-lg bubble-typing "
|
||||
style={{
|
||||
width: isTyping ? '4rem' : '100%',
|
||||
height: isTyping ? '2rem' : '100%',
|
||||
}}
|
||||
data-testid="host-bubble"
|
||||
>
|
||||
{isTyping ? <TypingBubble /> : null}
|
||||
</div>
|
||||
{block.type === BubbleBlockType.TEXT && (
|
||||
<p
|
||||
style={{
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
className={
|
||||
'overflow-hidden content-opacity mx-4 my-2 whitespace-pre-wrap slate-html-container relative ' +
|
||||
(isTyping ? 'opacity-0 h-6' : 'opacity-100 h-full')
|
||||
}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: content,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TextBubble } from './components/TextBubble'
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TypingEmulation } from '@typebot.io/schemas'
|
||||
|
||||
export const computeTypingDuration = (
|
||||
bubbleContent: string,
|
||||
typingSettings: TypingEmulation
|
||||
) => {
|
||||
let wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
|
||||
if (wordCount === 0) wordCount = bubbleContent.length
|
||||
const typedWordsPerMinute = typingSettings.speed
|
||||
let typingTimeout = typingSettings.enabled
|
||||
? (wordCount / typedWordsPerMinute) * 60000
|
||||
: 0
|
||||
if (typingTimeout > typingSettings.maxDelay * 1000)
|
||||
typingTimeout = typingSettings.maxDelay * 1000
|
||||
return typingTimeout
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import {
|
||||
Variable,
|
||||
VideoBubbleContent,
|
||||
VideoBubbleContentType,
|
||||
VideoBubbleBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
|
||||
type Props = {
|
||||
block: VideoBubbleBlock
|
||||
onTransitionEnd: () => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
|
||||
export const mediaLoadingFallbackTimeout = 5000
|
||||
|
||||
export const VideoBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
const { typebot, isLoading } = useTypebot()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const onTypingEnd = useCallback(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}, [onTransitionEnd])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
const timeout = setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
onTypingEnd()
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [isLoading, isTyping, onTypingEnd])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" 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'}>
|
||||
<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 ? <TypingBubble /> : <></>}
|
||||
</div>
|
||||
<VideoContent
|
||||
content={block.content}
|
||||
isTyping={isTyping}
|
||||
variables={typebot.variables}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const VideoContent = ({
|
||||
content,
|
||||
isTyping,
|
||||
variables,
|
||||
}: {
|
||||
content?: VideoBubbleContent
|
||||
isTyping: boolean
|
||||
variables: Variable[]
|
||||
}) => {
|
||||
const [url] = useState(parseVariables(variables)(content?.url))
|
||||
|
||||
if (!content?.type) return <></>
|
||||
|
||||
switch (content.type) {
|
||||
case VideoBubbleContentType.URL: {
|
||||
const isSafariBrowser = window.navigator.vendor.match(/apple/i)
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
className={
|
||||
'p-4 focus:outline-none w-full z-10 content-opacity rounded-md ' +
|
||||
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||
}
|
||||
style={{
|
||||
height: isTyping ? '2rem' : 'auto',
|
||||
maxHeight: isSafariBrowser ? '40vh' : '',
|
||||
}}
|
||||
autoPlay
|
||||
>
|
||||
<source src={url} type="video/mp4" />
|
||||
Sorry, your browser doesn't support embedded videos.
|
||||
</video>
|
||||
)
|
||||
}
|
||||
case VideoBubbleContentType.VIMEO:
|
||||
case VideoBubbleContentType.YOUTUBE: {
|
||||
const baseUrl =
|
||||
content.type === VideoBubbleContentType.VIMEO
|
||||
? 'https://player.vimeo.com/video'
|
||||
: 'https://www.youtube.com/embed'
|
||||
return (
|
||||
<iframe
|
||||
src={`${baseUrl}/${content.id}`}
|
||||
className={
|
||||
'w-full p-4 content-opacity z-10 rounded-md ' +
|
||||
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||
}
|
||||
height={isTyping ? '2rem' : '200px'}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { VideoBubble } from './components/VideoBubble'
|
||||
Reference in New Issue
Block a user