Skip to content

Commit

Permalink
fix(engine): 🐛 Chat chunk management
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Mar 3, 2022
1 parent 831150e commit 4714e80
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 68 deletions.
25 changes: 13 additions & 12 deletions apps/builder/contexts/TypebotContext/actions/edges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -122,16 +123,16 @@ export const cleanUpEdgeDraft = (
typebot: WritableDraft<Typebot>,
deletedNodeId: string
) => {
typebot.edges = typebot.edges.filter(
(edge) =>
![
edge.from.blockId,
edge.from.stepId,
edge.from.itemId,
edge.to.blockId,
edge.to.stepId,
].includes(deletedNodeId)
const edgesToDelete = typebot.edges.filter((edge) =>
[
edge.from.blockId,
edge.from.stepId,
edge.from.itemId,
edge.to.blockId,
edge.to.stepId,
].includes(deletedNodeId)
)
edgesToDelete.forEach((edge) => deleteEdgeDraft(typebot, edge.id))
}

const removeExistingEdge = (
Expand Down
2 changes: 1 addition & 1 deletion apps/builder/playwright/tests/bubbles/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
)
Expand Down
155 changes: 105 additions & 50 deletions packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { AvatarSideContainer } from './AvatarSideContainer'
import { useTypebot } from '../../contexts/TypebotContext'
import {
isBubbleStep,
isBubbleStepType,
isChoiceInput,
isDefined,
isInputStep,
isIntegrationStep,
isLogicStep,
Expand All @@ -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[]
Expand All @@ -25,6 +28,8 @@ type ChatBlockProps = {
onBlockEnd: (edgeId?: string) => void
}

type ChatDisplayChunk = { bubbles: BubbleStep[]; input?: InputStep }

export const ChatBlock = ({
steps,
startStepIndex,
Expand All @@ -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(
Expand Down Expand Up @@ -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 &&
Expand All @@ -118,58 +144,87 @@ 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

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)
}
/>
)}
<TransitionGroup>
{bubbleSteps.map((step) => (
<CSSTransition
key={step.id}
classNames="bubble"
timeout={500}
unmountOnExit
>
<HostBubble step={step} onTransitionEnd={displayNextStep} />
</CSSTransition>
))}
</TransitionGroup>
</div>
{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}
/>
))}
</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>
{inputSteps.map((step) => (
{bubbles.map((step) => (
<CSSTransition
key={step.id}
classNames="bubble"
timeout={500}
unmountOnExit
>
<InputChatStep
step={step}
onTransitionEnd={displayNextStep}
hasAvatar={typebot.theme.chat.hostAvatar?.isEnabled ?? true}
/>
<HostBubble step={step} onTransitionEnd={onDisplayNextStep} />
</CSSTransition>
))}
</TransitionGroup>
</div>
</div>
<CSSTransition
classNames="bubble"
timeout={500}
unmountOnExit
in={isDefined(input)}
>
{input && (
<InputChatStep
step={input}
onTransitionEnd={onDisplayNextStep}
hasAvatar={hostAvatar.isEnabled}
/>
)}
</CSSTransition>
</>
)
}
10 changes: 6 additions & 4 deletions packages/bot-engine/src/components/ConversationContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ export const ConversationContainer = ({

const autoScrollToBottom = () => {
if (!scrollableContainer.current) return
scroll.scrollToBottom({
duration: 500,
container: scrollableContainer.current,
})
setTimeout(() => {
scroll.scrollToBottom({
duration: 500,
container: scrollableContainer.current,
})
}, 1)
}

return (
Expand Down
19 changes: 18 additions & 1 deletion packages/bot-engine/src/services/chat.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
}

2 comments on commit 4714e80

@vercel
Copy link

@vercel vercel bot commented on 4714e80 Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 4714e80 Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
builder-v2-git-main-typebot-io.vercel.app
app.typebot.io

Please sign in to comment.