1
0
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

This commit is contained in:
Nuno Góis 2024-10-18 12:10:48 +01:00
parent 9f0c438f36
commit c825c58db8
No known key found for this signature in database
GPG Key ID: 71ECC689F1091765
2 changed files with 74 additions and 11 deletions

View File

@ -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,21 +92,44 @@ export const AIChat = () => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const isAtEndRef = useRef(true);
const chatEndRef = useRef<HTMLDivElement | null>(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]);
scrollToEnd();
const intersectionObserver = new IntersectionObserver(
([entry]) => {
isAtEndRef.current = entry.isIntersecting;
},
{ threshold: 1.0 },
);
const target = chatEndRef.current;
if (target) {
intersectionObserver.observe(target);
}
return () => {
if (target) {
intersectionObserver.unobserve(target);
}
};
}, [open]);
useEffect(() => {
scrollToEnd();
}, [open]);
scrollToEnd({ behavior: 'smooth', onlyIfAtEnd: true });
}, [messages]);
const onSend = async (content: string) => {
if (!content.trim() || loading) return;
@ -153,7 +180,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 })}
>
<StyledChat>
<AIChatHeader
@ -176,7 +203,13 @@ export const AIChat = () => {
)}
<div ref={chatEndRef} />
</StyledChatContent>
<AIChatInput onSend={onSend} loading={loading} />
<AIChatInput
onSend={onSend}
loading={loading}
onHeightChange={() =>
scrollToEnd({ onlyIfAtEnd: true })
}
/>
</StyledChat>
</StyledResizable>
</StyledAIChatContainer>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import {
IconButton,
InputAdornment,
@ -32,11 +32,41 @@ 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<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?.();
}
});
const target = inputContainerRef.current;
if (target) {
resizeObserver.observe(target);
}
return () => {
if (target) {
resizeObserver.unobserve(target);
}
};
}, [onHeightChange]);
const send = () => {
if (!message.trim() || loading) return;
onSend(message);
@ -44,7 +74,7 @@ export const AIChatInput = ({ onSend, loading }: IAIChatInputProps) => {
};
return (
<StyledAIChatInputContainer>
<StyledAIChatInputContainer ref={inputContainerRef}>
<StyledAIChatInput
autoFocus
size='small'