1
0
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:
Kamran Ahmed
2025-06-10 13:33:08 +01:00
parent 5051534c9d
commit cca2c1bd36
2 changed files with 75 additions and 10 deletions

View File

@@ -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 ? (

View File

@@ -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(() => {
scrollareaRef.current?.scrollTo({
top: scrollareaRef.current.scrollHeight,
behavior: 'instant',
});
}, [scrollareaRef]);
const scrollToBottom = useCallback(
(behavior: 'smooth' | 'instant' = 'smooth') => {
scrollareaRef.current?.scrollTo({
top: scrollareaRef.current.scrollHeight,
behavior,
});
},
[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,