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.`, 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,44 @@ 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 },
);
const target = chatEndRef.current;
if (target) {
intersectionObserver.observe(target);
}
return () => {
if (target) {
intersectionObserver.unobserve(target);
}
};
}, [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 +180,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 +203,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>

View File

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