fix(engine): 🐛 Chat chunk management
This commit is contained in:
@ -90,15 +90,16 @@ export const deleteEdgeDraft = (
|
||||
) => {
|
||||
const edgeIndex = typebot.edges.findIndex(byId(edgeId))
|
||||
if (edgeIndex === -1) return
|
||||
deleteOutgoingEdgeIdProps(typebot, edgeIndex)
|
||||
deleteOutgoingEdgeIdProps(typebot, edgeId)
|
||||
typebot.edges.splice(edgeIndex, 1)
|
||||
}
|
||||
|
||||
const deleteOutgoingEdgeIdProps = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
edgeIndex: number
|
||||
edgeId: string
|
||||
) => {
|
||||
const edge = typebot.edges[edgeIndex]
|
||||
const edge = typebot.edges.find(byId(edgeId))
|
||||
if (!edge) return
|
||||
const fromBlockIndex = typebot.blocks.findIndex(byId(edge.from.blockId))
|
||||
const fromStepIndex = typebot.blocks[fromBlockIndex].steps.findIndex(
|
||||
byId(edge.from.stepId)
|
||||
@ -122,9 +123,8 @@ export const cleanUpEdgeDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
deletedNodeId: string
|
||||
) => {
|
||||
typebot.edges = typebot.edges.filter(
|
||||
(edge) =>
|
||||
![
|
||||
const edgesToDelete = typebot.edges.filter((edge) =>
|
||||
[
|
||||
edge.from.blockId,
|
||||
edge.from.stepId,
|
||||
edge.from.itemId,
|
||||
@ -132,6 +132,7 @@ export const cleanUpEdgeDraft = (
|
||||
edge.to.stepId,
|
||||
].includes(deletedNodeId)
|
||||
)
|
||||
edgesToDelete.forEach((edge) => deleteEdgeDraft(typebot, edge.id))
|
||||
}
|
||||
|
||||
const removeExistingEdge = (
|
||||
|
@ -35,7 +35,7 @@ test.describe.parallel('Image bubble step', () => {
|
||||
await expect(page.locator('img')).toHaveAttribute(
|
||||
'src',
|
||||
new RegExp(
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/typebots/${typebotId}/avatar.jpg`,
|
||||
`http://localhost:9000/typebot/public/typebots/${typebotId}/avatar.jpg`,
|
||||
'gm'
|
||||
)
|
||||
)
|
||||
|
@ -4,7 +4,9 @@ import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import {
|
||||
isBubbleStep,
|
||||
isBubbleStepType,
|
||||
isChoiceInput,
|
||||
isDefined,
|
||||
isInputStep,
|
||||
isIntegrationStep,
|
||||
isLogicStep,
|
||||
@ -17,6 +19,7 @@ import { useAnswers } from 'contexts/AnswersContext'
|
||||
import { BubbleStep, InputStep, Step } from 'models'
|
||||
import { HostBubble } from './ChatStep/bubbles/HostBubble'
|
||||
import { InputChatStep } from './ChatStep/InputChatStep'
|
||||
import { getLastChatStepType } from 'services/chat'
|
||||
|
||||
type ChatBlockProps = {
|
||||
steps: Step[]
|
||||
@ -25,6 +28,8 @@ type ChatBlockProps = {
|
||||
onBlockEnd: (edgeId?: string) => void
|
||||
}
|
||||
|
||||
type ChatDisplayChunk = { bubbles: BubbleStep[]; input?: InputStep }
|
||||
|
||||
export const ChatBlock = ({
|
||||
steps,
|
||||
startStepIndex,
|
||||
@ -40,30 +45,52 @@ export const ChatBlock = ({
|
||||
onNewLog,
|
||||
} = useTypebot()
|
||||
const { resultValues } = useAnswers()
|
||||
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>()
|
||||
const [processedSteps, setProcessedSteps] = useState<Step[]>([])
|
||||
const [displayedChunks, setDisplayedChunks] = useState<ChatDisplayChunk[]>([])
|
||||
|
||||
const insertStepInStack = (nextStep: Step) => {
|
||||
setProcessedSteps([...processedSteps, nextStep])
|
||||
if (isBubbleStep(nextStep)) {
|
||||
const lastStepType = getLastChatStepType(processedSteps)
|
||||
lastStepType && isBubbleStepType(lastStepType)
|
||||
? setDisplayedChunks(
|
||||
displayedChunks.map((c, idx) =>
|
||||
idx === displayedChunks.length - 1
|
||||
? { bubbles: [...c.bubbles, nextStep] }
|
||||
: c
|
||||
)
|
||||
)
|
||||
: setDisplayedChunks([...displayedChunks, { bubbles: [nextStep] }])
|
||||
}
|
||||
if (isInputStep(nextStep)) {
|
||||
return displayedChunks.length === 0 ||
|
||||
isDefined(displayedChunks[displayedChunks.length - 1].input)
|
||||
? setDisplayedChunks([
|
||||
...displayedChunks,
|
||||
{ bubbles: [], input: nextStep },
|
||||
])
|
||||
: setDisplayedChunks(
|
||||
displayedChunks.map((c, idx) =>
|
||||
idx === displayedChunks.length - 1 ? { ...c, input: nextStep } : c
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const nextStep = steps[startStepIndex]
|
||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||
if (nextStep) insertStepInStack(nextStep)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
avatarSideContainerRef.current?.refreshTopOffset()
|
||||
onScroll()
|
||||
onNewStepDisplayed()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [displayedSteps])
|
||||
}, [processedSteps])
|
||||
|
||||
const onNewStepDisplayed = async () => {
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
const currentStep = [...processedSteps].pop()
|
||||
if (!currentStep) return
|
||||
if (isLogicStep(currentStep)) {
|
||||
const nextEdgeId = executeLogic(
|
||||
@ -95,13 +122,12 @@ export const ChatBlock = ({
|
||||
|
||||
const displayNextStep = (answerContent?: string, isRetry?: boolean) => {
|
||||
onScroll()
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
const currentStep = [...processedSteps].pop()
|
||||
if (currentStep) {
|
||||
if (isRetry && stepCanBeRetried(currentStep))
|
||||
return setDisplayedSteps([
|
||||
...displayedSteps,
|
||||
parseRetryStep(currentStep, typebot.variables, createEdge),
|
||||
])
|
||||
return insertStepInStack(
|
||||
parseRetryStep(currentStep, typebot.variables, createEdge)
|
||||
)
|
||||
if (
|
||||
isInputStep(currentStep) &&
|
||||
currentStep.options?.variableId &&
|
||||
@ -118,11 +144,11 @@ export const ChatBlock = ({
|
||||
if (nextEdgeId) return onBlockEnd(nextEdgeId)
|
||||
}
|
||||
|
||||
if (currentStep?.outgoingEdgeId || displayedSteps.length === steps.length)
|
||||
if (currentStep?.outgoingEdgeId || processedSteps.length === steps.length)
|
||||
return onBlockEnd(currentStep.outgoingEdgeId)
|
||||
}
|
||||
const nextStep = steps[displayedSteps.length]
|
||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||
const nextStep = steps[processedSteps.length]
|
||||
if (nextStep) insertStepInStack(nextStep)
|
||||
}
|
||||
|
||||
const avatarSrc = typebot.theme.chat.hostAvatar?.url
|
||||
@ -130,46 +156,75 @@ export const ChatBlock = ({
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<div className="flex flex-col w-full min-w-0">
|
||||
<div className="flex">
|
||||
{bubbleSteps.length > 0 &&
|
||||
(typebot.theme.chat.hostAvatar?.isEnabled ?? true) && (
|
||||
<AvatarSideContainer
|
||||
ref={avatarSideContainerRef}
|
||||
hostAvatarSrc={
|
||||
avatarSrc && parseVariables(typebot.variables)(avatarSrc)
|
||||
}
|
||||
{displayedChunks.map((chunk, idx) => (
|
||||
<ChatChunks
|
||||
key={idx}
|
||||
displayChunk={chunk}
|
||||
hostAvatar={{
|
||||
isEnabled: typebot.theme.chat.hostAvatar?.isEnabled ?? true,
|
||||
src: avatarSrc && parseVariables(typebot.variables)(avatarSrc),
|
||||
}}
|
||||
onDisplayNextStep={displayNextStep}
|
||||
/>
|
||||
)}
|
||||
<TransitionGroup>
|
||||
{bubbleSteps.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<HostBubble step={step} onTransitionEnd={displayNextStep} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
displayChunk: ChatDisplayChunk
|
||||
hostAvatar: { isEnabled: boolean; src?: string }
|
||||
onDisplayNextStep: (answerContent?: string, isRetry?: boolean) => void
|
||||
}
|
||||
const ChatChunks = ({
|
||||
displayChunk: { bubbles, input },
|
||||
hostAvatar,
|
||||
onDisplayNextStep,
|
||||
}: Props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const avatarSideContainerRef = useRef<any>()
|
||||
|
||||
useEffect(() => {
|
||||
avatarSideContainerRef.current?.refreshTopOffset()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
{hostAvatar.isEnabled && (
|
||||
<AvatarSideContainer
|
||||
ref={avatarSideContainerRef}
|
||||
hostAvatarSrc={hostAvatar.src}
|
||||
/>
|
||||
)}
|
||||
<TransitionGroup>
|
||||
{bubbles.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<HostBubble step={step} onTransitionEnd={onDisplayNextStep} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<CSSTransition
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
in={isDefined(input)}
|
||||
>
|
||||
{input && (
|
||||
<InputChatStep
|
||||
step={input}
|
||||
onTransitionEnd={onDisplayNextStep}
|
||||
hasAvatar={hostAvatar.isEnabled}
|
||||
/>
|
||||
)}
|
||||
</CSSTransition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -75,10 +75,12 @@ export const ConversationContainer = ({
|
||||
|
||||
const autoScrollToBottom = () => {
|
||||
if (!scrollableContainer.current) return
|
||||
setTimeout(() => {
|
||||
scroll.scrollToBottom({
|
||||
duration: 500,
|
||||
container: scrollableContainer.current,
|
||||
})
|
||||
}, 1)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { TypingEmulation } from 'models'
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepType,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
Step,
|
||||
TypingEmulation,
|
||||
} from 'models'
|
||||
import { isBubbleStep, isInputStep } from 'utils'
|
||||
|
||||
export const computeTypingTimeout = (
|
||||
bubbleContent: string,
|
||||
@ -14,3 +22,12 @@ export const computeTypingTimeout = (
|
||||
typingTimeout = typingSettings.maxDelay * 1000
|
||||
return typingTimeout
|
||||
}
|
||||
|
||||
export const getLastChatStepType = (
|
||||
steps: Step[]
|
||||
): BubbleStepType | InputStepType | undefined => {
|
||||
const displayedSteps = steps.filter(
|
||||
(s) => isBubbleStep(s) || isInputStep(s)
|
||||
) as (BubbleStep | InputStep)[]
|
||||
return displayedSteps.pop()?.type
|
||||
}
|
||||
|
Reference in New Issue
Block a user