1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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:
Nuno Góis 2024-10-18 14:38:46 +01:00 committed by GitHub
parent 1c29f70edc
commit ffcfe85575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 89 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,43 @@ 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 },
);
if (chatEndRef.current) {
intersectionObserver.observe(chatEndRef.current);
}
return () => {
if (chatEndRef.current) {
intersectionObserver.unobserve(chatEndRef.current);
}
};
}, [open]);
useEffect(() => {
scrollToEnd();
}, [open]);
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 })}
>
<StyledChat>
<AIChatHeader
@ -176,7 +202,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,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<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 = () => {
if (!message.trim() || loading) return;
onSend(message);
@ -44,7 +73,7 @@ export const AIChatInput = ({ onSend, loading }: IAIChatInputProps) => {
};
return (
<StyledAIChatInputContainer>
<StyledAIChatInputContainer ref={inputContainerRef}>
<StyledAIChatInput
autoFocus
size='small'

View File

@ -9,10 +9,27 @@ class ResizeObserver {
disconnect() {}
}
class IntersectionObserver {
root: any;
rootMargin: any;
thresholds: any;
observe() {}
unobserve() {}
disconnect() {}
takeRecords() {
return [];
}
}
if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserver;
}
if (!window.IntersectionObserver) {
window.IntersectionObserver = IntersectionObserver;
}
process.env.TZ = 'UTC';
const errorsToIgnore = [