Added use stick to bottom on page

This commit is contained in:
Lucas Oliveira 2025-10-21 18:10:57 -03:00
parent a4ab95e891
commit f9692aba5e

View file

@ -2,6 +2,7 @@
import { Loader2, Zap } from "lucide-react"; import { Loader2, Zap } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
import { ProtectedRoute } from "@/components/protected-route"; import { ProtectedRoute } from "@/components/protected-route";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { type EndpointType, useChat } from "@/contexts/chat-context"; import { type EndpointType, useChat } from "@/contexts/chat-context";
@ -71,8 +72,10 @@ function ChatPage() {
x: number; x: number;
y: number; y: number;
} | null>(null); } | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const chatInputRef = useRef<ChatInputHandle>(null); const chatInputRef = useRef<ChatInputHandle>(null);
const { scrollToBottom } = useStickToBottomContext();
const lastLoadedConversationRef = useRef<string | null>(null); const lastLoadedConversationRef = useRef<string | null>(null);
const { addTask } = useTask(); const { addTask } = useTask();
const { selectedFilter, parsedFilterData, setSelectedFilter } = const { selectedFilter, parsedFilterData, setSelectedFilter } =
@ -82,7 +85,6 @@ function ChatPage() {
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"; const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow";
const { const {
streamingMessage, streamingMessage,
isLoading: isStreamingLoading,
sendMessage: sendStreamingMessage, sendMessage: sendStreamingMessage,
abortStream, abortStream,
} = useChatStreaming({ } = useChatStreaming({
@ -119,10 +121,6 @@ function ChatPage() {
}, },
}); });
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
const getCursorPosition = (textarea: HTMLTextAreaElement) => { const getCursorPosition = (textarea: HTMLTextAreaElement) => {
// Create a hidden div with the same styles as the textarea // Create a hidden div with the same styles as the textarea
const div = document.createElement("div"); const div = document.createElement("div");
@ -357,21 +355,10 @@ function ChatPage() {
} }
}; };
useEffect(() => {
// Only auto-scroll if not in the middle of user interaction
if (!isUserInteracting) {
const timer = setTimeout(() => {
scrollToBottom();
}, 50); // Small delay to avoid conflicts with click events
return () => clearTimeout(timer);
}
}, [messages, streamingMessage, isUserInteracting]);
// Reset selected index when search term changes // Reset selected index when search term changes
useEffect(() => { useEffect(() => {
setSelectedFilterIndex(0); setSelectedFilterIndex(0);
}, [filterSearchTerm]); }, []);
// Auto-focus the input on component mount // Auto-focus the input on component mount
useEffect(() => { useEffect(() => {
@ -408,7 +395,7 @@ function ChatPage() {
window.removeEventListener("newConversation", handleNewConversation); window.removeEventListener("newConversation", handleNewConversation);
window.removeEventListener("focusInput", handleFocusInput); window.removeEventListener("focusInput", handleFocusInput);
}; };
}, [abortStream]); }, [abortStream, setLoading]);
// Load conversation only when user explicitly selects a conversation // Load conversation only when user explicitly selects a conversation
useEffect(() => { useEffect(() => {
@ -709,7 +696,7 @@ function ChatPage() {
handleFileUploadError as EventListener, handleFileUploadError as EventListener,
); );
}; };
}, [endpoint, setPreviousResponseIds]); }, [endpoint, setPreviousResponseIds, setLoading]);
const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery( const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery(
previousResponseIds[endpoint], previousResponseIds[endpoint],
@ -749,6 +736,10 @@ function ChatPage() {
limit: parsedFilterData?.limit ?? 10, limit: parsedFilterData?.limit ?? 10,
scoreThreshold: parsedFilterData?.scoreThreshold ?? 0, scoreThreshold: parsedFilterData?.scoreThreshold ?? 0,
}); });
scrollToBottom({
animation: "smooth",
duration: 1000,
});
}; };
const handleSendMessage = async (inputMessage: string) => { const handleSendMessage = async (inputMessage: string) => {
@ -765,6 +756,11 @@ function ChatPage() {
setLoading(true); setLoading(true);
setIsFilterHighlighted(false); setIsFilterHighlighted(false);
scrollToBottom({
animation: "smooth",
duration: 1000,
});
if (asyncMode) { if (asyncMode) {
await handleSSEStream(userMessage); await handleSSEStream(userMessage);
} else { } else {
@ -1113,66 +1109,57 @@ function ChatPage() {
} }
}; };
return ( return (<>
<div className="flex flex-col h-full"> {/* Debug header - only show in debug mode */}
{/* Debug header - only show in debug mode */} {isDebugMode && (
{isDebugMode && ( <div className="flex items-center justify-between p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center gap-2"></div>
<div className="flex items-center gap-2"></div> <div className="flex items-center gap-4">
<div className="flex items-center gap-4"> {/* Async Mode Toggle */}
{/* Async Mode Toggle */} <div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1">
<div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1"> <Button
<Button variant={!asyncMode ? "default" : "ghost"}
variant={!asyncMode ? "default" : "ghost"} size="sm"
size="sm" onClick={() => setAsyncMode(false)}
onClick={() => setAsyncMode(false)} className="h-7 text-xs"
className="h-7 text-xs" >
> Streaming Off
Streaming Off </Button>
</Button> <Button
<Button variant={asyncMode ? "default" : "ghost"}
variant={asyncMode ? "default" : "ghost"} size="sm"
size="sm" onClick={() => setAsyncMode(true)}
onClick={() => setAsyncMode(true)} className="h-7 text-xs"
className="h-7 text-xs" >
> <Zap className="h-3 w-3 mr-1" />
<Zap className="h-3 w-3 mr-1" /> Streaming On
Streaming On </Button>
</Button> </div>
</div> {/* Endpoint Toggle */}
{/* Endpoint Toggle */} <div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1">
<div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1"> <Button
<Button variant={endpoint === "chat" ? "default" : "ghost"}
variant={endpoint === "chat" ? "default" : "ghost"} size="sm"
size="sm" onClick={() => handleEndpointChange("chat")}
onClick={() => handleEndpointChange("chat")} className="h-7 text-xs"
className="h-7 text-xs" >
> Chat
Chat </Button>
</Button> <Button
<Button variant={endpoint === "langflow" ? "default" : "ghost"}
variant={endpoint === "langflow" ? "default" : "ghost"} size="sm"
size="sm" onClick={() => handleEndpointChange("langflow")}
onClick={() => handleEndpointChange("langflow")} className="h-7 text-xs"
className="h-7 text-xs" >
> Langflow
Langflow </Button>
</Button> </div>
</div> </div>
</div> </div>
</div> )}
)}
<div <StickToBottom.Content className="flex flex-col min-h-full overflow-x-hidden p-6">
className={`flex-1 flex flex-col min-h-0 px-6 ${ <div className="flex flex-col place-self-center space-y-6 max-w-[960px] w-full mx-auto">
!isDebugMode ? "pt-6" : ""
}`}
>
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-hidden">
{/* Messages Area */}
<div
className={`flex-1 overflow-y-auto overflow-x-hidden scrollbar-hide space-y-6 min-h-0 transition-all relative`}
>
{messages.length === 0 && !streamingMessage ? ( {messages.length === 0 && !streamingMessage ? (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center"> <div className="text-center">
@ -1190,7 +1177,12 @@ function ChatPage() {
) : ( ) : (
<> <>
{messages.map((message, index) => ( {messages.map((message, index) => (
<div key={index} className="space-y-6 group"> <div
key={`${
message.role
}-${index}-${message.timestamp?.getTime()}`}
className="space-y-6 group"
>
{message.role === "user" && ( {message.role === "user" && (
<UserMessage content={message.content} /> <UserMessage content={message.content} />
)} )}
@ -1220,55 +1212,58 @@ function ChatPage() {
isStreaming isStreaming
/> />
)} )}
<div ref={messagesEndRef} />
</> </>
)} )}
{!streamingMessage && (
<Nudges
nudges={loading ? [] : (nudges as string[])}
handleSuggestionClick={handleSuggestionClick}
/>
)}
</div> </div>
</div> </StickToBottom.Content>
</div>
{/* Suggestion chips - always show unless streaming */} {/* Input Area - Fixed at bottom */}
{!streamingMessage && ( <ChatInput
<Nudges ref={chatInputRef}
nudges={loading ? [] : (nudges as string[])} input={input}
handleSuggestionClick={handleSuggestionClick} loading={loading}
/> isUploading={isUploading}
)} selectedFilter={selectedFilter}
isFilterDropdownOpen={isFilterDropdownOpen}
{/* Input Area - Fixed at bottom */} availableFilters={availableFilters}
<ChatInput filterSearchTerm={filterSearchTerm}
ref={chatInputRef} selectedFilterIndex={selectedFilterIndex}
input={input} anchorPosition={anchorPosition}
loading={loading} textareaHeight={textareaHeight}
isUploading={isUploading} parsedFilterData={parsedFilterData}
selectedFilter={selectedFilter} onSubmit={handleSubmit}
isFilterDropdownOpen={isFilterDropdownOpen} onChange={onChange}
availableFilters={availableFilters} onKeyDown={handleKeyDown}
filterSearchTerm={filterSearchTerm} onHeightChange={(height) => setTextareaHeight(height)}
selectedFilterIndex={selectedFilterIndex} onFilterSelect={handleFilterSelect}
anchorPosition={anchorPosition} onAtClick={onAtClick}
textareaHeight={textareaHeight} onFilePickerChange={handleFilePickerChange}
parsedFilterData={parsedFilterData} onFilePickerClick={handleFilePickerClick}
onSubmit={handleSubmit} setSelectedFilter={setSelectedFilter}
onChange={onChange} setIsFilterHighlighted={setIsFilterHighlighted}
onKeyDown={handleKeyDown} setIsFilterDropdownOpen={setIsFilterDropdownOpen}
onHeightChange={(height) => setTextareaHeight(height)} /></>
onFilterSelect={handleFilterSelect}
onAtClick={onAtClick}
onFilePickerChange={handleFilePickerChange}
onFilePickerClick={handleFilePickerClick}
setSelectedFilter={setSelectedFilter}
setIsFilterHighlighted={setIsFilterHighlighted}
setIsFilterDropdownOpen={setIsFilterDropdownOpen}
/>
</div>
); );
} }
export default function ProtectedChatPage() { export default function ProtectedChatPage() {
return ( return (
<ProtectedRoute> <ProtectedRoute>
<div className="flex w-full h-full overflow-hidden">
<StickToBottom
className="flex h-full flex-1 flex-col"
resize="smooth"
initial="instant"
mass={1}
>
<ChatPage /> <ChatPage />
</StickToBottom></div>
</ProtectedRoute> </ProtectedRoute>
); );
} }