mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	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.
This commit is contained in:
		
							parent
							
								
									1c29f70edc
								
							
						
					
					
						commit
						ffcfe85575
					
				@ -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.`,
 | 
					    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;
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ScrollOptions = ScrollIntoViewOptions & {
 | 
				
			||||||
 | 
					    onlyIfAtEnd?: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledAIIconContainer = styled('div')(({ theme }) => ({
 | 
					const StyledAIIconContainer = styled('div')(({ theme }) => ({
 | 
				
			||||||
    position: 'fixed',
 | 
					    position: 'fixed',
 | 
				
			||||||
    bottom: 20,
 | 
					    bottom: 20,
 | 
				
			||||||
@ -88,21 +92,43 @@ export const AIChat = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const [messages, setMessages] = useState<ChatMessage[]>([]);
 | 
					    const [messages, setMessages] = useState<ChatMessage[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isAtEndRef = useRef(true);
 | 
				
			||||||
    const chatEndRef = useRef<HTMLDivElement | null>(null);
 | 
					    const chatEndRef = useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const scrollToEnd = (options?: ScrollIntoViewOptions) => {
 | 
					    const scrollToEnd = (options?: ScrollOptions) => {
 | 
				
			||||||
        if (chatEndRef.current) {
 | 
					        if (chatEndRef.current) {
 | 
				
			||||||
 | 
					            const shouldScroll = !options?.onlyIfAtEnd || isAtEndRef.current;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (shouldScroll) {
 | 
				
			||||||
                chatEndRef.current.scrollIntoView(options);
 | 
					                chatEndRef.current.scrollIntoView(options);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        scrollToEnd({ behavior: 'smooth' });
 | 
					        scrollToEnd();
 | 
				
			||||||
    }, [messages]);
 | 
					
 | 
				
			||||||
 | 
					        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(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        scrollToEnd();
 | 
					        scrollToEnd({ behavior: 'smooth', onlyIfAtEnd: true });
 | 
				
			||||||
    }, [open]);
 | 
					    }, [messages]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onSend = async (content: string) => {
 | 
					    const onSend = async (content: string) => {
 | 
				
			||||||
        if (!content.trim() || loading) return;
 | 
					        if (!content.trim() || loading) return;
 | 
				
			||||||
@ -153,7 +179,7 @@ export const AIChat = () => {
 | 
				
			|||||||
                minSize={{ width: '270px', height: '200px' }}
 | 
					                minSize={{ width: '270px', height: '200px' }}
 | 
				
			||||||
                maxSize={{ width: '90vw', height: '90vh' }}
 | 
					                maxSize={{ width: '90vw', height: '90vh' }}
 | 
				
			||||||
                defaultSize={{ width: '320px', height: '450px' }}
 | 
					                defaultSize={{ width: '320px', height: '450px' }}
 | 
				
			||||||
                onResize={scrollToEnd}
 | 
					                onResize={() => scrollToEnd({ onlyIfAtEnd: true })}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                <StyledChat>
 | 
					                <StyledChat>
 | 
				
			||||||
                    <AIChatHeader
 | 
					                    <AIChatHeader
 | 
				
			||||||
@ -176,7 +202,13 @@ export const AIChat = () => {
 | 
				
			|||||||
                        )}
 | 
					                        )}
 | 
				
			||||||
                        <div ref={chatEndRef} />
 | 
					                        <div ref={chatEndRef} />
 | 
				
			||||||
                    </StyledChatContent>
 | 
					                    </StyledChatContent>
 | 
				
			||||||
                    <AIChatInput onSend={onSend} loading={loading} />
 | 
					                    <AIChatInput
 | 
				
			||||||
 | 
					                        onSend={onSend}
 | 
				
			||||||
 | 
					                        loading={loading}
 | 
				
			||||||
 | 
					                        onHeightChange={() =>
 | 
				
			||||||
 | 
					                            scrollToEnd({ onlyIfAtEnd: true })
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
                </StyledChat>
 | 
					                </StyledChat>
 | 
				
			||||||
            </StyledResizable>
 | 
					            </StyledResizable>
 | 
				
			||||||
        </StyledAIChatContainer>
 | 
					        </StyledAIChatContainer>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useState } from 'react';
 | 
					import { useEffect, useRef, useState } from 'react';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    IconButton,
 | 
					    IconButton,
 | 
				
			||||||
    InputAdornment,
 | 
					    InputAdornment,
 | 
				
			||||||
@ -32,11 +32,40 @@ const StyledIconButton = styled(IconButton)({
 | 
				
			|||||||
export interface IAIChatInputProps {
 | 
					export interface IAIChatInputProps {
 | 
				
			||||||
    onSend: (message: string) => void;
 | 
					    onSend: (message: string) => void;
 | 
				
			||||||
    loading: boolean;
 | 
					    loading: boolean;
 | 
				
			||||||
 | 
					    onHeightChange?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AIChatInput = ({ onSend, loading }: IAIChatInputProps) => {
 | 
					export const AIChatInput = ({
 | 
				
			||||||
 | 
					    onSend,
 | 
				
			||||||
 | 
					    loading,
 | 
				
			||||||
 | 
					    onHeightChange,
 | 
				
			||||||
 | 
					}: IAIChatInputProps) => {
 | 
				
			||||||
    const [message, setMessage] = useState('');
 | 
					    const [message, setMessage] = useState('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inputContainerRef = useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
 | 
					    const previousHeightRef = useRef<number>(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 = () => {
 | 
					    const send = () => {
 | 
				
			||||||
        if (!message.trim() || loading) return;
 | 
					        if (!message.trim() || loading) return;
 | 
				
			||||||
        onSend(message);
 | 
					        onSend(message);
 | 
				
			||||||
@ -44,7 +73,7 @@ export const AIChatInput = ({ onSend, loading }: IAIChatInputProps) => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <StyledAIChatInputContainer>
 | 
					        <StyledAIChatInputContainer ref={inputContainerRef}>
 | 
				
			||||||
            <StyledAIChatInput
 | 
					            <StyledAIChatInput
 | 
				
			||||||
                autoFocus
 | 
					                autoFocus
 | 
				
			||||||
                size='small'
 | 
					                size='small'
 | 
				
			||||||
 | 
				
			|||||||
@ -9,10 +9,27 @@ class ResizeObserver {
 | 
				
			|||||||
    disconnect() {}
 | 
					    disconnect() {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IntersectionObserver {
 | 
				
			||||||
 | 
					    root: any;
 | 
				
			||||||
 | 
					    rootMargin: any;
 | 
				
			||||||
 | 
					    thresholds: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    observe() {}
 | 
				
			||||||
 | 
					    unobserve() {}
 | 
				
			||||||
 | 
					    disconnect() {}
 | 
				
			||||||
 | 
					    takeRecords() {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (!window.ResizeObserver) {
 | 
					if (!window.ResizeObserver) {
 | 
				
			||||||
    window.ResizeObserver = ResizeObserver;
 | 
					    window.ResizeObserver = ResizeObserver;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!window.IntersectionObserver) {
 | 
				
			||||||
 | 
					    window.IntersectionObserver = IntersectionObserver;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
process.env.TZ = 'UTC';
 | 
					process.env.TZ = 'UTC';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const errorsToIgnore = [
 | 
					const errorsToIgnore = [
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user