diff --git a/package.json b/package.json index bcff832..529691d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "author": "ItzCrazyKns", "scripts": { - "dev": "next dev", + "dev": "next dev --turbopack", "build": "npm run db:push && next build", "start": "next start", "lint": "next lint", diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 0cbf1f6..34ff08e 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -5,12 +5,13 @@ import MessageInput from './MessageInput'; import { File, Message } from './ChatWindow'; import MessageBox from './MessageBox'; import MessageBoxLoading from './MessageBoxLoading'; +import { check } from 'drizzle-orm/gel-core'; const Chat = ({ loading, messages, sendMessage, - messageAppeared, + scrollTrigger, rewrite, fileIds, setFileIds, @@ -29,7 +30,7 @@ const Chat = ({ }, ) => void; loading: boolean; - messageAppeared: boolean; + scrollTrigger: number; rewrite: (messageId: string) => void; fileIds: string[]; setFileIds: (fileIds: string[]) => void; @@ -39,8 +40,72 @@ const Chat = ({ setOptimizationMode: (mode: string) => void; }) => { const [dividerWidth, setDividerWidth] = useState(0); + const [isAtBottom, setIsAtBottom] = useState(true); + const [manuallyScrolledUp, setManuallyScrolledUp] = useState(false); const dividerRef = useRef(null); const messageEnd = useRef(null); + const SCROLL_THRESHOLD = 200; // pixels from bottom to consider "at bottom" + + // Check if user is at bottom of page + useEffect(() => { + const checkIsAtBottom = () => { + const position = window.innerHeight + window.scrollY; + const height = document.body.scrollHeight; + const atBottom = position >= height - SCROLL_THRESHOLD; + + setIsAtBottom(atBottom); + }; + + // Initial check + checkIsAtBottom(); + + // Add scroll event listener + window.addEventListener('scroll', checkIsAtBottom); + + return () => { + window.removeEventListener('scroll', checkIsAtBottom); + }; + }, []); + + // Detect wheel and touch events to identify user's scrolling direction + useEffect(() => { + const checkIsAtBottom = () => { + const position = window.innerHeight + window.scrollY; + const height = document.body.scrollHeight; + const atBottom = position >= height - SCROLL_THRESHOLD; + + // If user scrolls to bottom, reset the manuallyScrolledUp flag + if (atBottom) { + setManuallyScrolledUp(false); + } + + setIsAtBottom(atBottom); + }; + + const handleWheel = (e: WheelEvent) => { + // Positive deltaY means scrolling down, negative means scrolling up + if (e.deltaY < 0) { + // User is scrolling up + setManuallyScrolledUp(true); + } else if (e.deltaY > 0) { + checkIsAtBottom(); + } + }; + + const handleTouchStart = (e: TouchEvent) => { + // Immediately stop auto-scrolling on any touch interaction + setManuallyScrolledUp(true); + }; + + // Add event listeners + window.addEventListener('wheel', handleWheel, { passive: true }); + window.addEventListener('touchstart', handleTouchStart, { passive: true }); + + return () => { + window.removeEventListener('wheel', handleWheel); + window.removeEventListener('touchstart', handleTouchStart); + }; + }, [isAtBottom]); useEffect(() => { const updateDividerWidth = () => { @@ -58,19 +123,36 @@ const Chat = ({ }; }); + // Scroll when user sends a message useEffect(() => { const scroll = () => { - messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); + messageEnd.current?.scrollIntoView({behavior: 'smooth'}); }; if (messages.length === 1) { document.title = `${messages[0].content.substring(0, 30)} - Perplexica`; } - if (messages[messages.length - 1]?.role == 'user') { + // Always scroll when user sends a message + if (messages[messages.length - 1]?.role === 'user') { scroll(); + setIsAtBottom(true); // Reset to true when user sends a message + setManuallyScrolledUp(false); // Reset manually scrolled flag when user sends a message } }, [messages]); + + // Auto-scroll for assistant responses only if user is at bottom and hasn't manually scrolled up + useEffect(() => { + const position = window.innerHeight + window.scrollY; + const height = document.body.scrollHeight; + const atBottom = position >= height - SCROLL_THRESHOLD; + console.log('scrollTrigger', scrollTrigger); + setIsAtBottom(atBottom); + + if (isAtBottom && !manuallyScrolledUp && messages.length > 0) { + messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [scrollTrigger, isAtBottom, messages.length, manuallyScrolledUp]); return (
@@ -96,13 +178,34 @@ const Chat = ({ ); })} - {loading && !messageAppeared && } + {loading && }
+ {dividerWidth > 0 && (
+ {/* Scroll to bottom button - appears above the MessageInput when user has scrolled up */} + {(manuallyScrolledUp && !isAtBottom) && ( +
+ +
+ )} + { }, []); const [loading, setLoading] = useState(false); - const [messageAppeared, setMessageAppeared] = useState(false); + const [scrollTrigger, setScrollTrigger] = useState(0); const [chatHistory, setChatHistory] = useState<[string, string][]>([]); const [messages, setMessages] = useState([]); @@ -351,6 +351,7 @@ const ChatWindow = ({ id }: { id?: string }) => { suggestions?: string[]; }, ) => { + setScrollTrigger(x => x === 0 ? -1 : 0); // Special case: If we're just updating an existing message with suggestions if (options?.suggestions && options.messageId) { setMessages((prev) => @@ -371,7 +372,6 @@ const ChatWindow = ({ id }: { id?: string }) => { } setLoading(true); - setMessageAppeared(false); let sources: Document[] | undefined = undefined; let recievedMessage = ''; @@ -389,6 +389,8 @@ const ChatWindow = ({ id }: { id?: string }) => { messages.length > 2 ? rewriteIndex - 1 : 0, ); setChatHistory(messageChatHistory); + + setScrollTrigger(prev => prev + 1); } const messageId = @@ -427,8 +429,8 @@ const ChatWindow = ({ id }: { id?: string }) => { }, ]); added = true; + setScrollTrigger(prev => prev + 1); } - setMessageAppeared(true); } if (data.type === 'message') { @@ -461,7 +463,7 @@ const ChatWindow = ({ id }: { id?: string }) => { ); recievedMessage += data.data; - setMessageAppeared(true); + setScrollTrigger(prev => prev + 1); } if (data.type === 'messageEnd') { @@ -486,6 +488,7 @@ const ChatWindow = ({ id }: { id?: string }) => { ); setLoading(false); + setScrollTrigger(prev => prev + 1); const lastMsg = messagesRef.current[messagesRef.current.length - 1]; @@ -634,7 +637,7 @@ const ChatWindow = ({ id }: { id?: string }) => { loading={loading} messages={messages} sendMessage={sendMessage} - messageAppeared={messageAppeared} + scrollTrigger={scrollTrigger} rewrite={rewrite} fileIds={fileIds} setFileIds={setFileIds}