From ffcfe855752a17eac6b1ffaea3ad1bc6f7236d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 18 Oct 2024 14:38:46 +0100 Subject: [PATCH] chore: scroll-related UX adjustments in the Unleash AI chat (#8478) https://linear.app/unleash/issue/2-2857/make-some-scroll-related-ux-adjustments-to-the-unleash-ai-chat Introduces scroll-related UX enhancements to the Unleash AI chat, providing a smoother and more refined user experience. --- frontend/src/component/ai/AIChat.tsx | 48 +++++++++++++++++++---- frontend/src/component/ai/AIChatInput.tsx | 35 +++++++++++++++-- frontend/src/setupTests.ts | 17 ++++++++ 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/frontend/src/component/ai/AIChat.tsx b/frontend/src/component/ai/AIChat.tsx index 9fea0bf4d233..13e3ad91a4e0 100644 --- a/frontend/src/component/ai/AIChat.tsx +++ b/frontend/src/component/ai/AIChat.tsx @@ -20,6 +20,10 @@ const AI_ERROR_MESSAGE = { content: `I'm sorry, I'm having trouble understanding you right now. I've reported the issue to the team. Please try again later.`, } as const; +type ScrollOptions = ScrollIntoViewOptions & { + onlyIfAtEnd?: boolean; +}; + const StyledAIIconContainer = styled('div')(({ theme }) => ({ position: 'fixed', bottom: 20, @@ -88,22 +92,44 @@ export const AIChat = () => { const [messages, setMessages] = useState([]); + const isAtEndRef = useRef(true); const chatEndRef = useRef(null); - const scrollToEnd = (options?: ScrollIntoViewOptions) => { + const scrollToEnd = (options?: ScrollOptions) => { if (chatEndRef.current) { - chatEndRef.current.scrollIntoView(options); + const shouldScroll = !options?.onlyIfAtEnd || isAtEndRef.current; + + if (shouldScroll) { + chatEndRef.current.scrollIntoView(options); + } } }; - useEffect(() => { - scrollToEnd({ behavior: 'smooth' }); - }, [messages]); - useEffect(() => { scrollToEnd(); + + const intersectionObserver = new IntersectionObserver( + ([entry]) => { + isAtEndRef.current = entry.isIntersecting; + }, + { threshold: 1.0 }, + ); + + if (chatEndRef.current) { + intersectionObserver.observe(chatEndRef.current); + } + + return () => { + if (chatEndRef.current) { + intersectionObserver.unobserve(chatEndRef.current); + } + }; }, [open]); + useEffect(() => { + scrollToEnd({ behavior: 'smooth', onlyIfAtEnd: true }); + }, [messages]); + const onSend = async (content: string) => { if (!content.trim() || loading) return; @@ -153,7 +179,7 @@ export const AIChat = () => { minSize={{ width: '270px', height: '200px' }} maxSize={{ width: '90vw', height: '90vh' }} defaultSize={{ width: '320px', height: '450px' }} - onResize={scrollToEnd} + onResize={() => scrollToEnd({ onlyIfAtEnd: true })} > { )}
- + + scrollToEnd({ onlyIfAtEnd: true }) + } + /> diff --git a/frontend/src/component/ai/AIChatInput.tsx b/frontend/src/component/ai/AIChatInput.tsx index cc8ffbf1d06e..ba900c16649d 100644 --- a/frontend/src/component/ai/AIChatInput.tsx +++ b/frontend/src/component/ai/AIChatInput.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { IconButton, InputAdornment, @@ -32,11 +32,40 @@ const StyledIconButton = styled(IconButton)({ export interface IAIChatInputProps { onSend: (message: string) => void; loading: boolean; + onHeightChange?: () => void; } -export const AIChatInput = ({ onSend, loading }: IAIChatInputProps) => { +export const AIChatInput = ({ + onSend, + loading, + onHeightChange, +}: IAIChatInputProps) => { const [message, setMessage] = useState(''); + const inputContainerRef = useRef(null); + const previousHeightRef = useRef(0); + + useEffect(() => { + const resizeObserver = new ResizeObserver(([entry]) => { + const newHeight = entry.contentRect.height; + + if (newHeight !== previousHeightRef.current) { + previousHeightRef.current = newHeight; + onHeightChange?.(); + } + }); + + if (inputContainerRef.current) { + resizeObserver.observe(inputContainerRef.current); + } + + return () => { + if (inputContainerRef.current) { + resizeObserver.unobserve(inputContainerRef.current); + } + }; + }, [onHeightChange]); + const send = () => { if (!message.trim() || loading) return; onSend(message); @@ -44,7 +73,7 @@ export const AIChatInput = ({ onSend, loading }: IAIChatInputProps) => { }; return ( - +