mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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) {
|
||||||
chatEndRef.current.scrollIntoView(options);
|
const shouldScroll = !options?.onlyIfAtEnd || isAtEndRef.current;
|
||||||
|
|
||||||
|
if (shouldScroll) {
|
||||||
|
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