feat(engine): ✨ Can edit answers by clicking on it
This commit is contained in:
@@ -1,56 +1,69 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, {
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { Avatar } from '../avatars/Avatar'
|
import { Avatar } from '../avatars/Avatar'
|
||||||
import { useFrame } from 'react-frame-component'
|
import { useFrame } from 'react-frame-component'
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
import { CSSTransition } from 'react-transition-group'
|
||||||
import { useHostAvatars } from '../../contexts/HostAvatarsContext'
|
|
||||||
|
|
||||||
export const AvatarSideContainer = ({
|
type Props = { hostAvatarSrc?: string }
|
||||||
hostAvatarSrc,
|
|
||||||
}: {
|
|
||||||
hostAvatarSrc?: string
|
|
||||||
}) => {
|
|
||||||
const { lastBubblesTopOffset } = useHostAvatars()
|
|
||||||
const { window, document } = useFrame()
|
|
||||||
const [marginBottom, setMarginBottom] = useState(
|
|
||||||
window.innerWidth < 400 ? 38 : 48
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const AvatarSideContainer = forwardRef(
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
({ hostAvatarSrc }: Props, ref: ForwardedRef<unknown>) => {
|
||||||
const isMobile = window.innerWidth < 400
|
const { document } = useFrame()
|
||||||
setMarginBottom(isMobile ? 38 : 48)
|
const [show, setShow] = useState(false)
|
||||||
})
|
const [avatarTopOffset, setAvatarTopOffset] = useState(0)
|
||||||
resizeObserver.observe(document.body)
|
|
||||||
return () => {
|
const refreshTopOffset = () => {
|
||||||
resizeObserver.disconnect()
|
if (!scrollingSideBlockRef.current || !avatarContainer.current) return
|
||||||
|
const { height } = scrollingSideBlockRef.current.getBoundingClientRect()
|
||||||
|
const { height: avatarHeight } =
|
||||||
|
avatarContainer.current.getBoundingClientRect()
|
||||||
|
setAvatarTopOffset(height - avatarHeight)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const scrollingSideBlockRef = useRef<HTMLDivElement>(null)
|
||||||
}, [])
|
const avatarContainer = useRef<HTMLDivElement>(null)
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refreshTopOffset,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="flex w-6 xs:w-10 mr-2 flex-shrink-0 items-center">
|
setShow(true)
|
||||||
<TransitionGroup>
|
const resizeObserver = new ResizeObserver(refreshTopOffset)
|
||||||
{lastBubblesTopOffset
|
resizeObserver.observe(document.body)
|
||||||
.filter((n) => n !== -1)
|
return () => {
|
||||||
.map((topOffset, idx) => (
|
resizeObserver.disconnect()
|
||||||
<CSSTransition
|
}
|
||||||
key={idx}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
classNames="bubble"
|
}, [])
|
||||||
timeout={500}
|
|
||||||
unmountOnExit
|
return (
|
||||||
>
|
<div
|
||||||
<div
|
className="flex w-6 xs:w-10 mr-2 mb-2 flex-shrink-0 items-center relative "
|
||||||
className="absolute w-6 h-6 xs:w-10 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
|
ref={scrollingSideBlockRef}
|
||||||
style={{
|
>
|
||||||
top: `calc(${topOffset}px - ${marginBottom}px)`,
|
<CSSTransition
|
||||||
transition: 'top 350ms ease-out',
|
classNames="bubble"
|
||||||
}}
|
timeout={500}
|
||||||
>
|
in={show}
|
||||||
<Avatar avatarSrc={hostAvatarSrc} />
|
unmountOnExit
|
||||||
</div>
|
>
|
||||||
</CSSTransition>
|
<div
|
||||||
))}
|
className="absolute w-6 xs:w-10 h-6 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
|
||||||
</TransitionGroup>
|
ref={avatarContainer}
|
||||||
</div>
|
style={{
|
||||||
)
|
top: `${avatarTopOffset}px`,
|
||||||
}
|
transition: 'top 350ms ease-out, opacity 500ms',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar avatarSrc={hostAvatarSrc} />
|
||||||
|
</div>
|
||||||
|
</CSSTransition>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||||
import { ChatStep } from './ChatStep'
|
|
||||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
|
||||||
import { useTypebot } from '../../contexts/TypebotContext'
|
import { useTypebot } from '../../contexts/TypebotContext'
|
||||||
import {
|
import {
|
||||||
isBubbleStep,
|
isBubbleStep,
|
||||||
@@ -16,7 +14,9 @@ import { executeIntegration } from 'services/integration'
|
|||||||
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||||
import { parseVariables } from 'index'
|
import { parseVariables } from 'index'
|
||||||
import { useAnswers } from 'contexts/AnswersContext'
|
import { useAnswers } from 'contexts/AnswersContext'
|
||||||
import { Step } from 'models'
|
import { BubbleStep, InputStep, Step } from 'models'
|
||||||
|
import { HostBubble } from './ChatStep/bubbles/HostBubble'
|
||||||
|
import { InputChatStep } from './ChatStep/InputChatStep'
|
||||||
|
|
||||||
type ChatBlockProps = {
|
type ChatBlockProps = {
|
||||||
steps: Step[]
|
steps: Step[]
|
||||||
@@ -41,6 +41,13 @@ export const ChatBlock = ({
|
|||||||
} = useTypebot()
|
} = useTypebot()
|
||||||
const { resultValues } = useAnswers()
|
const { resultValues } = useAnswers()
|
||||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||||
|
const bubbleSteps = displayedSteps.filter((step) =>
|
||||||
|
isBubbleStep(step)
|
||||||
|
) as BubbleStep[]
|
||||||
|
const inputSteps = displayedSteps.filter((step) =>
|
||||||
|
isInputStep(step)
|
||||||
|
) as InputStep[]
|
||||||
|
const avatarSideContainerRef = useRef<any>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextStep = steps[startStepIndex]
|
const nextStep = steps[startStepIndex]
|
||||||
@@ -49,6 +56,7 @@ export const ChatBlock = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
avatarSideContainerRef.current?.refreshTopOffset()
|
||||||
onScroll()
|
onScroll()
|
||||||
onNewStepDisplayed()
|
onNewStepDisplayed()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -118,33 +126,49 @@ export const ChatBlock = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const avatarSrc = typebot.theme.chat.hostAvatar?.url
|
const avatarSrc = typebot.theme.chat.hostAvatar?.url
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<HostAvatarsContext>
|
<div className="flex flex-col w-full min-w-0">
|
||||||
{(typebot.theme.chat.hostAvatar?.isEnabled ?? true) && (
|
<div className="flex">
|
||||||
<AvatarSideContainer
|
{(typebot.theme.chat.hostAvatar?.isEnabled ?? true) && (
|
||||||
hostAvatarSrc={
|
<AvatarSideContainer
|
||||||
avatarSrc && parseVariables(typebot.variables)(avatarSrc)
|
ref={avatarSideContainerRef}
|
||||||
}
|
hostAvatarSrc={
|
||||||
/>
|
avatarSrc && parseVariables(typebot.variables)(avatarSrc)
|
||||||
)}
|
}
|
||||||
<div className="flex flex-col w-full min-w-0">
|
/>
|
||||||
|
)}
|
||||||
<TransitionGroup>
|
<TransitionGroup>
|
||||||
{displayedSteps
|
{bubbleSteps.map((step) => (
|
||||||
.filter((step) => isInputStep(step) || isBubbleStep(step))
|
<CSSTransition
|
||||||
.map((step) => (
|
key={step.id}
|
||||||
<CSSTransition
|
classNames="bubble"
|
||||||
key={step.id}
|
timeout={500}
|
||||||
classNames="bubble"
|
unmountOnExit
|
||||||
timeout={500}
|
>
|
||||||
unmountOnExit
|
<HostBubble step={step} onTransitionEnd={displayNextStep} />
|
||||||
>
|
</CSSTransition>
|
||||||
<ChatStep step={step} onTransitionEnd={displayNextStep} />
|
))}
|
||||||
</CSSTransition>
|
|
||||||
))}
|
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</HostAvatarsContext>
|
<TransitionGroup>
|
||||||
|
{inputSteps.map((step) => (
|
||||||
|
<CSSTransition
|
||||||
|
key={step.id}
|
||||||
|
classNames="bubble"
|
||||||
|
timeout={500}
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<InputChatStep
|
||||||
|
step={step}
|
||||||
|
onTransitionEnd={displayNextStep}
|
||||||
|
hasAvatar={typebot.theme.chat.hostAvatar?.isEnabled ?? true}
|
||||||
|
/>
|
||||||
|
</CSSTransition>
|
||||||
|
))}
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
import { useAnswers } from '../../../contexts/AnswersContext'
|
||||||
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
import { InputStep, InputStepType } from 'models'
|
||||||
import { InputStep, InputStepType, Step } from 'models'
|
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
import { TextForm } from './inputs/TextForm'
|
import { TextForm } from './inputs/TextForm'
|
||||||
import { byId, isBubbleStep, isInputStep } from 'utils'
|
import { byId } from 'utils'
|
||||||
import { DateForm } from './inputs/DateForm'
|
import { DateForm } from './inputs/DateForm'
|
||||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||||
import { HostBubble } from './bubbles/HostBubble'
|
|
||||||
import { isInputValid } from 'services/inputs'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { parseVariables } from 'index'
|
import { parseVariables } from 'index'
|
||||||
|
import { isInputValid } from 'services/inputs'
|
||||||
|
|
||||||
export const ChatStep = ({
|
export const InputChatStep = ({
|
||||||
step,
|
step,
|
||||||
|
hasAvatar,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
}: {
|
}: {
|
||||||
step: Step
|
step: InputStep
|
||||||
|
hasAvatar: boolean
|
||||||
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
||||||
}) => {
|
}) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
const { addAnswer } = useAnswers()
|
const { addAnswer } = useAnswers()
|
||||||
|
const [answer, setAnswer] = useState<string>()
|
||||||
|
const [isEditting, setIsEditting] = useState(false)
|
||||||
|
|
||||||
const handleInputSubmit = (
|
const { variableId } = step.options
|
||||||
content: string,
|
const defaultValue =
|
||||||
isRetry: boolean,
|
variableId && typebot.variables.find(byId(variableId))?.value
|
||||||
variableId?: string
|
|
||||||
) => {
|
const handleSubmit = (content: string) => {
|
||||||
|
setAnswer(content)
|
||||||
|
const isRetry = !isInputValid(content, step.type)
|
||||||
if (!isRetry)
|
if (!isRetry)
|
||||||
addAnswer({
|
addAnswer({
|
||||||
stepId: step.id,
|
stepId: step.id,
|
||||||
@@ -33,38 +38,13 @@ export const ChatStep = ({
|
|||||||
content,
|
content,
|
||||||
variableId: variableId ?? null,
|
variableId: variableId ?? null,
|
||||||
})
|
})
|
||||||
onTransitionEnd(content, isRetry)
|
if (!isEditting) onTransitionEnd(content, isRetry)
|
||||||
|
setIsEditting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBubbleStep(step))
|
const handleGuestBubbleClick = () => {
|
||||||
return <HostBubble step={step} onTransitionEnd={onTransitionEnd} />
|
setAnswer(undefined)
|
||||||
if (isInputStep(step))
|
setIsEditting(true)
|
||||||
return <InputChatStep step={step} onSubmit={handleInputSubmit} />
|
|
||||||
return <span>No step</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
const InputChatStep = ({
|
|
||||||
step,
|
|
||||||
onSubmit,
|
|
||||||
}: {
|
|
||||||
step: InputStep
|
|
||||||
onSubmit: (value: string, isRetry: boolean, variableId?: string) => void
|
|
||||||
}) => {
|
|
||||||
const { typebot } = useTypebot()
|
|
||||||
const { addNewAvatarOffset } = useHostAvatars()
|
|
||||||
const [answer, setAnswer] = useState<string>()
|
|
||||||
const { variableId } = step.options
|
|
||||||
const defaultValue =
|
|
||||||
variableId && typebot.variables.find(byId(variableId))?.value
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
addNewAvatarOffset()
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSubmit = (value: string) => {
|
|
||||||
setAnswer(value)
|
|
||||||
onSubmit(value, !isInputValid(value, step.type), step.options.variableId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
@@ -74,9 +54,30 @@ const InputChatStep = ({
|
|||||||
message={answer}
|
message={answer}
|
||||||
showAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
showAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
||||||
avatarSrc={avatarUrl && parseVariables(typebot.variables)(avatarUrl)}
|
avatarSrc={avatarUrl && parseVariables(typebot.variables)(avatarUrl)}
|
||||||
|
onClick={handleGuestBubbleClick}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
{hasAvatar && (
|
||||||
|
<div className="flex w-6 xs:w-10 h-6 xs:h-10 mr-2 mb-2 mt-1 flex-shrink-0 items-center" />
|
||||||
|
)}
|
||||||
|
<Input step={step} onSubmit={handleSubmit} defaultValue={defaultValue} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = ({
|
||||||
|
step,
|
||||||
|
onSubmit,
|
||||||
|
defaultValue,
|
||||||
|
}: {
|
||||||
|
step: InputStep
|
||||||
|
onSubmit: (value: string) => void
|
||||||
|
defaultValue?: string
|
||||||
|
}) => {
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case InputStepType.TEXT:
|
case InputStepType.TEXT:
|
||||||
case InputStepType.NUMBER:
|
case InputStepType.NUMBER:
|
||||||
@@ -84,15 +85,11 @@ const InputChatStep = ({
|
|||||||
case InputStepType.URL:
|
case InputStepType.URL:
|
||||||
case InputStepType.PHONE:
|
case InputStepType.PHONE:
|
||||||
return (
|
return (
|
||||||
<TextForm
|
<TextForm step={step} onSubmit={onSubmit} defaultValue={defaultValue} />
|
||||||
step={step}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
case InputStepType.DATE:
|
case InputStepType.DATE:
|
||||||
return <DateForm options={step.options} onSubmit={handleSubmit} />
|
return <DateForm options={step.options} onSubmit={onSubmit} />
|
||||||
case InputStepType.CHOICE:
|
case InputStepType.CHOICE:
|
||||||
return <ChoiceForm step={step} onSubmit={handleSubmit} />
|
return <ChoiceForm step={step} onSubmit={onSubmit} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,18 +6,20 @@ interface Props {
|
|||||||
message: string
|
message: string
|
||||||
showAvatar: boolean
|
showAvatar: boolean
|
||||||
avatarSrc?: string
|
avatarSrc?: string
|
||||||
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GuestBubble = ({
|
export const GuestBubble = ({
|
||||||
message,
|
message,
|
||||||
showAvatar,
|
showAvatar,
|
||||||
avatarSrc,
|
avatarSrc,
|
||||||
|
onClick,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<CSSTransition classNames="bubble" timeout={1000}>
|
<CSSTransition classNames="bubble" timeout={1000}>
|
||||||
<div className="flex justify-end mb-2 items-end">
|
<div className="flex justify-end mb-2 items-end" onClick={onClick}>
|
||||||
<span
|
<span
|
||||||
className="px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
className="px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble cursor-pointer hover:brightness-90 active:brightness-75"
|
||||||
data-testid="guest-bubble"
|
data-testid="guest-bubble"
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { ImageBubbleStep } from 'models'
|
import { ImageBubbleStep } from 'models'
|
||||||
import { TypingContent } from './TypingContent'
|
import { TypingContent } from './TypingContent'
|
||||||
@@ -16,7 +15,6 @@ export const mediaLoadingFallbackTimeout = 5000
|
|||||||
|
|
||||||
export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { updateLastAvatarOffset } = useHostAvatars()
|
|
||||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||||
const image = useRef<HTMLImageElement | null>(null)
|
const image = useRef<HTMLImageElement | null>(null)
|
||||||
const [isTyping, setIsTyping] = useState(true)
|
const [isTyping, setIsTyping] = useState(true)
|
||||||
@@ -27,7 +25,6 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendAvatarOffset()
|
|
||||||
showContentAfterMediaLoad()
|
showContentAfterMediaLoad()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
@@ -48,19 +45,10 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
|||||||
const onTypingEnd = () => {
|
const onTypingEnd = () => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendAvatarOffset()
|
|
||||||
onTransitionEnd()
|
onTransitionEnd()
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendAvatarOffset = () => {
|
|
||||||
if (!messageContainer.current) return
|
|
||||||
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
|
||||||
updateLastAvatarOffset(
|
|
||||||
messageContainer.current.offsetTop + containerDimensions.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col" ref={messageContainer}>
|
<div className="flex flex-col" ref={messageContainer}>
|
||||||
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { BubbleStepType, TextBubbleStep } from 'models'
|
import { BubbleStepType, TextBubbleStep } from 'models'
|
||||||
import { computeTypingTimeout } from 'services/chat'
|
import { computeTypingTimeout } from 'services/chat'
|
||||||
@@ -21,7 +20,6 @@ const defaultTypingEmulation = {
|
|||||||
|
|
||||||
export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { updateLastAvatarOffset } = useHostAvatars()
|
|
||||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||||
const [isTyping, setIsTyping] = useState(true)
|
const [isTyping, setIsTyping] = useState(true)
|
||||||
|
|
||||||
@@ -32,7 +30,6 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendAvatarOffset()
|
|
||||||
const typingTimeout = computeTypingTimeout(
|
const typingTimeout = computeTypingTimeout(
|
||||||
step.content.plainText,
|
step.content.plainText,
|
||||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||||
@@ -46,19 +43,10 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
|||||||
const onTypingEnd = () => {
|
const onTypingEnd = () => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendAvatarOffset()
|
|
||||||
onTransitionEnd()
|
onTransitionEnd()
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendAvatarOffset = () => {
|
|
||||||
if (!messageContainer.current) return
|
|
||||||
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
|
||||||
updateLastAvatarOffset(
|
|
||||||
messageContainer.current.offsetTop + containerDimensions.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col" ref={messageContainer}>
|
<div className="flex flex-col" ref={messageContainer}>
|
||||||
<div className="flex mb-2 w-full items-center">
|
<div className="flex mb-2 w-full items-center">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import {
|
import {
|
||||||
Variable,
|
Variable,
|
||||||
@@ -21,12 +20,10 @@ export const mediaLoadingFallbackTimeout = 5000
|
|||||||
|
|
||||||
export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
|
export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { updateLastAvatarOffset } = useHostAvatars()
|
|
||||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||||
const [isTyping, setIsTyping] = useState(true)
|
const [isTyping, setIsTyping] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendAvatarOffset()
|
|
||||||
showContentAfterMediaLoad()
|
showContentAfterMediaLoad()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
@@ -41,19 +38,10 @@ export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
|
|||||||
const onTypingEnd = () => {
|
const onTypingEnd = () => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendAvatarOffset()
|
|
||||||
onTransitionEnd()
|
onTransitionEnd()
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendAvatarOffset = () => {
|
|
||||||
if (!messageContainer.current) return
|
|
||||||
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
|
||||||
updateLastAvatarOffset(
|
|
||||||
messageContainer.current.offsetTop + containerDimensions.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col" ref={messageContainer}>
|
<div className="flex flex-col" ref={messageContainer}>
|
||||||
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { ChatStep } from './ChatStep'
|
export { InputChatStep } from './InputChatStep'
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const TextForm = ({ step, onSubmit, defaultValue }: TextFormProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full lg:w-4/6">
|
<div className="flex flex-col w-full lg:w-4/6 mb-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<form
|
<form
|
||||||
className="flex items-end justify-between rounded-lg pr-2 typebot-input"
|
className="flex items-end justify-between rounded-lg pr-2 typebot-input"
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
|
||||||
|
|
||||||
// This context just keeps track of the top offset of host avatar
|
|
||||||
const hostAvatarsContext = createContext<{
|
|
||||||
lastBubblesTopOffset: number[]
|
|
||||||
addNewAvatarOffset: () => void
|
|
||||||
updateLastAvatarOffset: (newOffset: number) => void
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
}>({})
|
|
||||||
|
|
||||||
export const HostAvatarsContext = ({ children }: { children: ReactNode }) => {
|
|
||||||
const [lastBubblesTopOffset, setLastBubblesTopOffset] = useState<number[]>([
|
|
||||||
-1,
|
|
||||||
])
|
|
||||||
|
|
||||||
const updateLastAvatarOffset = (newOffset: number) => {
|
|
||||||
const offsets = [...lastBubblesTopOffset]
|
|
||||||
offsets[offsets.length - 1] = newOffset
|
|
||||||
setLastBubblesTopOffset(offsets)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addNewAvatarOffset = () =>
|
|
||||||
setLastBubblesTopOffset([...lastBubblesTopOffset, -1])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<hostAvatarsContext.Provider
|
|
||||||
value={{
|
|
||||||
lastBubblesTopOffset,
|
|
||||||
updateLastAvatarOffset,
|
|
||||||
addNewAvatarOffset,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</hostAvatarsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useHostAvatars = () => useContext(hostAvatarsContext)
|
|
||||||
Reference in New Issue
Block a user