🚸 Improve auto scroll behavior
Only trigger when the last element is entirely visible
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.2.75",
|
"version": "0.2.76",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
@ -22,7 +22,7 @@ type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
|
|||||||
streamingMessageId: ChatChunkType['streamingMessageId']
|
streamingMessageId: ChatChunkType['streamingMessageId']
|
||||||
isTransitionDisabled?: boolean
|
isTransitionDisabled?: boolean
|
||||||
onNewBubbleDisplayed: (blockId: string) => Promise<void>
|
onNewBubbleDisplayed: (blockId: string) => Promise<void>
|
||||||
onScrollToBottom: (top?: number) => void
|
onScrollToBottom: (ref?: HTMLDivElement, offset?: number) => void
|
||||||
onSubmit: (input?: string) => void
|
onSubmit: (input?: string) => void
|
||||||
onSkip: () => void
|
onSkip: () => void
|
||||||
onAllBubblesDisplayed: () => void
|
onAllBubblesDisplayed: () => void
|
||||||
@ -33,19 +33,17 @@ export const ChatChunk = (props: Props) => {
|
|||||||
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(
|
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(
|
||||||
props.isTransitionDisabled ? props.messages.length : 0
|
props.isTransitionDisabled ? props.messages.length : 0
|
||||||
)
|
)
|
||||||
const [lastBubbleOffsetTop, setLastBubbleOffsetTop] = createSignal<number>()
|
const [lastBubble, setLastBubble] = createSignal<HTMLDivElement>()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (props.streamingMessageId) return
|
if (props.streamingMessageId) return
|
||||||
if (props.messages.length === 0) {
|
if (props.messages.length === 0) {
|
||||||
props.onAllBubblesDisplayed()
|
props.onAllBubblesDisplayed()
|
||||||
}
|
}
|
||||||
props.onScrollToBottom(
|
props.onScrollToBottom(inputRef, 50)
|
||||||
inputRef?.offsetTop ? inputRef?.offsetTop - 50 : undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const displayNextMessage = async (bubbleOffsetTop?: number) => {
|
const displayNextMessage = async (bubbleRef?: HTMLDivElement) => {
|
||||||
if (
|
if (
|
||||||
(props.settings.typingEmulation?.delayBetweenBubbles ??
|
(props.settings.typingEmulation?.delayBetweenBubbles ??
|
||||||
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
|
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
|
||||||
@ -66,9 +64,9 @@ export const ChatChunk = (props: Props) => {
|
|||||||
? displayedMessageIndex()
|
? displayedMessageIndex()
|
||||||
: displayedMessageIndex() + 1
|
: displayedMessageIndex() + 1
|
||||||
)
|
)
|
||||||
props.onScrollToBottom(bubbleOffsetTop)
|
props.onScrollToBottom(bubbleRef)
|
||||||
if (displayedMessageIndex() === props.messages.length) {
|
if (displayedMessageIndex() === props.messages.length) {
|
||||||
setLastBubbleOffsetTop(bubbleOffsetTop)
|
setLastBubble(bubbleRef)
|
||||||
props.onAllBubblesDisplayed()
|
props.onAllBubblesDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,7 +141,7 @@ export const ChatChunk = (props: Props) => {
|
|||||||
defaultSettings.general.isInputPrefillEnabled
|
defaultSettings.general.isInputPrefillEnabled
|
||||||
}
|
}
|
||||||
hasError={props.hasError}
|
hasError={props.hasError}
|
||||||
onTransitionEnd={() => props.onScrollToBottom(lastBubbleOffsetTop())}
|
onTransitionEnd={() => props.onScrollToBottom(lastBubble())}
|
||||||
onSubmit={props.onSubmit}
|
onSubmit={props.onSubmit}
|
||||||
onSkip={props.onSkip}
|
onSkip={props.onSkip}
|
||||||
/>
|
/>
|
||||||
|
@ -224,13 +224,26 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoScrollToBottom = (offsetTop?: number) => {
|
const autoScrollToBottom = (lastElement?: HTMLDivElement, offset = 0) => {
|
||||||
const chunks = chatChunks()
|
if (!chatContainer) return
|
||||||
const lastChunkWasStreaming =
|
if (!lastElement) {
|
||||||
chunks.length >= 2 && chunks[chunks.length - 2].streamingMessageId
|
|
||||||
if (lastChunkWasStreaming) return
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chatContainer?.scrollTo(0, offsetTop ?? chatContainer.scrollHeight)
|
chatContainer?.scrollTo(0, chatContainer.scrollHeight)
|
||||||
|
}, 50)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lastElementRect = lastElement.getBoundingClientRect()
|
||||||
|
const containerRect = chatContainer.getBoundingClientRect()
|
||||||
|
const lastElementTopRelative =
|
||||||
|
lastElementRect.top - containerRect.top + chatContainer.scrollTop
|
||||||
|
|
||||||
|
const isLastElementInVisibleArea =
|
||||||
|
lastElementTopRelative < chatContainer.scrollTop + containerRect.height &&
|
||||||
|
lastElementTopRelative + lastElementRect.height > chatContainer.scrollTop
|
||||||
|
|
||||||
|
if (isLastElementInVisibleArea)
|
||||||
|
setTimeout(() => {
|
||||||
|
chatContainer?.scrollTo(0, lastElement.offsetTop - offset)
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ type Props = {
|
|||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
typingEmulation: Settings['typingEmulation']
|
typingEmulation: Settings['typingEmulation']
|
||||||
isTypingSkipped: boolean
|
isTypingSkipped: boolean
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
onCompleted: (reply?: string) => void
|
onCompleted: (reply?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import clsx from 'clsx'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: AudioBubbleBlock['content']
|
content: AudioBubbleBlock['content']
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAnimationDuration = 400
|
const showAnimationDuration = 400
|
||||||
@ -28,10 +28,7 @@ export const AudioBubble = (props: Props) => {
|
|||||||
if (isPlayed) return
|
if (isPlayed) return
|
||||||
isPlayed = true
|
isPlayed = true
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(
|
setTimeout(() => props.onTransitionEnd?.(ref), showAnimationDuration)
|
||||||
() => props.onTransitionEnd?.(ref?.offsetTop),
|
|
||||||
showAnimationDuration
|
|
||||||
)
|
|
||||||
}, typingDuration)
|
}, typingDuration)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { botContainerHeight } from '@/utils/botContainerHeightSignal'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: CustomEmbedBubbleProps['content']
|
content: CustomEmbedBubbleProps['content']
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
onCompleted: (reply?: string) => void
|
onCompleted: (reply?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,10 +43,7 @@ export const CustomEmbedBubble = (props: Props) => {
|
|||||||
|
|
||||||
typingTimeout = setTimeout(() => {
|
typingTimeout = setTimeout(() => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(
|
setTimeout(() => props.onTransitionEnd?.(ref), showAnimationDuration)
|
||||||
() => props.onTransitionEnd?.(ref?.offsetTop),
|
|
||||||
showAnimationDuration
|
|
||||||
)
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/b
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: EmbedBubbleBlock['content']
|
content: EmbedBubbleBlock['content']
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let typingTimeout: NodeJS.Timeout
|
let typingTimeout: NodeJS.Timeout
|
||||||
@ -24,7 +24,7 @@ export const EmbedBubble = (props: Props) => {
|
|||||||
typingTimeout = setTimeout(() => {
|
typingTimeout = setTimeout(() => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd?.(ref?.offsetTop)
|
props.onTransitionEnd?.(ref)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
@ -7,7 +7,7 @@ import { defaultImageBubbleContent } from '@typebot.io/schemas/features/blocks/b
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: ImageBubbleBlock['content']
|
content: ImageBubbleBlock['content']
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showAnimationDuration = 400
|
export const showAnimationDuration = 400
|
||||||
@ -27,7 +27,7 @@ export const ImageBubble = (props: Props) => {
|
|||||||
if (!isTyping()) return
|
if (!isTyping()) return
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd?.(ref?.offsetTop)
|
props.onTransitionEnd?.(ref)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ type Props = {
|
|||||||
content: TextBubbleBlock['content']
|
content: TextBubbleBlock['content']
|
||||||
typingEmulation: Settings['typingEmulation']
|
typingEmulation: Settings['typingEmulation']
|
||||||
isTypingSkipped: boolean
|
isTypingSkipped: boolean
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showAnimationDuration = 400
|
export const showAnimationDuration = 400
|
||||||
@ -28,7 +28,7 @@ export const TextBubble = (props: Props) => {
|
|||||||
if (!isTyping()) return
|
if (!isTyping()) return
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd?.(ref?.offsetTop)
|
props.onTransitionEnd?.(ref)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: VideoBubbleBlock['content']
|
content: VideoBubbleBlock['content']
|
||||||
onTransitionEnd?: (offsetTop?: number) => void
|
onTransitionEnd?: (ref?: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showAnimationDuration = 400
|
export const showAnimationDuration = 400
|
||||||
@ -39,7 +39,7 @@ export const VideoBubble = (props: Props) => {
|
|||||||
if (!isTyping()) return
|
if (!isTyping()) return
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd?.(ref?.offsetTop)
|
props.onTransitionEnd?.(ref)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}, typingDuration)
|
}, typingDuration)
|
||||||
})
|
})
|
||||||
|
@ -525,7 +525,6 @@ const setInputs = (
|
|||||||
: '0'
|
: '0'
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(hexToRgb(inputs?.border?.color ?? '').join(', '))
|
|
||||||
documentStyle.setProperty(
|
documentStyle.setProperty(
|
||||||
cssVariableNames.chat.inputs.borderColor,
|
cssVariableNames.chat.inputs.borderColor,
|
||||||
hexToRgb(inputs?.border?.color ?? '').join(', ')
|
hexToRgb(inputs?.border?.color ?? '').join(', ')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/nextjs",
|
"name": "@typebot.io/nextjs",
|
||||||
"version": "0.2.75",
|
"version": "0.2.76",
|
||||||
"description": "Convenient library to display typebots on your Next.js website",
|
"description": "Convenient library to display typebots on your Next.js website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.2.75",
|
"version": "0.2.76",
|
||||||
"description": "Convenient library to display typebots on your React app",
|
"description": "Convenient library to display typebots on your React app",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
Reference in New Issue
Block a user