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:
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.`,
|
||||
} 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>
|
||||
|
@ -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'
|
||||
|
@ -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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user