mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-03 06:12:53 +02:00
Add scroll to bottom functionality
This commit is contained in:
@@ -3,6 +3,7 @@ import type { JSONContent } from '@tiptap/core';
|
||||
import {
|
||||
AppWindow,
|
||||
BookOpen,
|
||||
ChevronDown,
|
||||
MessageCirclePlus,
|
||||
PauseCircleIcon,
|
||||
PersonStanding,
|
||||
@@ -142,6 +143,8 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
aiChatHistory,
|
||||
isStreamingMessage,
|
||||
streamedMessage,
|
||||
showScrollToBottom,
|
||||
setShowScrollToBottom,
|
||||
handleChatSubmit,
|
||||
handleAbort,
|
||||
scrollToBottom,
|
||||
@@ -208,7 +211,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="fixed z-50 inset-0 bg-black opacity-50"
|
||||
className="fixed inset-0 z-50 bg-black opacity-50"
|
||||
></div>
|
||||
)}
|
||||
|
||||
@@ -332,6 +335,20 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
<RoadmapAIChatCard role="assistant" jsx={streamedMessage} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scroll to bottom button */}
|
||||
{showScrollToBottom && (
|
||||
<button
|
||||
onClick={() => {
|
||||
scrollToBottom('instant');
|
||||
setShowScrollToBottom(false);
|
||||
}}
|
||||
className="sticky bottom-0 mx-auto mt-2 flex items-center gap-1.5 rounded-full bg-gray-900 px-3 py-1.5 text-xs text-white shadow-lg transition-all hover:bg-gray-800"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
Scroll to bottom
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
@@ -383,7 +400,7 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
setTimeout(() => scrollToBottom(), 0);
|
||||
setTimeout(() => scrollToBottom('instant'), 0);
|
||||
}}
|
||||
>
|
||||
{!hasMessages ? (
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState, useEffect } from 'react';
|
||||
import type { JSONContent } from '@tiptap/core';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { removeAuthToken } from '../lib/jwt';
|
||||
@@ -43,14 +43,60 @@ export function useRoadmapAIChat(options: Options) {
|
||||
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
|
||||
const [streamedMessage, setStreamedMessage] =
|
||||
useState<React.ReactNode | null>(null);
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const scrollToBottom = useCallback(
|
||||
(behavior: 'smooth' | 'instant' = 'smooth') => {
|
||||
scrollareaRef.current?.scrollTo({
|
||||
top: scrollareaRef.current.scrollHeight,
|
||||
behavior: 'instant',
|
||||
behavior,
|
||||
});
|
||||
}, [scrollareaRef]);
|
||||
},
|
||||
[scrollareaRef],
|
||||
);
|
||||
|
||||
// Check if user has scrolled away from bottom
|
||||
const checkScrollPosition = useCallback(() => {
|
||||
const scrollArea = scrollareaRef.current;
|
||||
if (!scrollArea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollArea;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; // 50px threshold
|
||||
setShowScrollToBottom(!isAtBottom && aiChatHistory.length > 0);
|
||||
}, [aiChatHistory.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollArea = scrollareaRef.current;
|
||||
if (!scrollArea) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollArea.addEventListener('scroll', checkScrollPosition);
|
||||
return () => scrollArea.removeEventListener('scroll', checkScrollPosition);
|
||||
}, [checkScrollPosition]);
|
||||
|
||||
// When user is already at the bottom and there is new message
|
||||
// being streamed, we keep scrolling to bottom to show the new message
|
||||
// unless user has scrolled up at which point we stop scrolling to bottom
|
||||
useEffect(() => {
|
||||
if (isStreamingMessage || streamedMessage) {
|
||||
const scrollArea = scrollareaRef.current;
|
||||
if (!scrollArea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollArea;
|
||||
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 100;
|
||||
|
||||
if (isNearBottom) {
|
||||
scrollToBottom('instant');
|
||||
setShowScrollToBottom(false);
|
||||
}
|
||||
}
|
||||
}, [isStreamingMessage, streamedMessage, scrollToBottom]);
|
||||
|
||||
const renderer: Record<string, MessagePartRenderer> = useMemo(
|
||||
() => ({
|
||||
@@ -175,7 +221,7 @@ export function useRoadmapAIChat(options: Options) {
|
||||
|
||||
setIsStreamingMessage(true);
|
||||
flushSync(() => setAiChatHistory(newMessages));
|
||||
scrollToBottom();
|
||||
scrollToBottom('instant');
|
||||
completeAITutorChat(newMessages, abortControllerRef.current);
|
||||
},
|
||||
[aiChatHistory, isStreamingMessage, scrollToBottom],
|
||||
@@ -195,6 +241,8 @@ export function useRoadmapAIChat(options: Options) {
|
||||
aiChatHistory,
|
||||
isStreamingMessage,
|
||||
streamedMessage,
|
||||
showScrollToBottom,
|
||||
setShowScrollToBottom,
|
||||
abortControllerRef,
|
||||
handleChatSubmit,
|
||||
handleAbort,
|
||||
|
Reference in New Issue
Block a user