Added use stick to bottom on page
This commit is contained in:
parent
a4ab95e891
commit
f9692aba5e
1 changed files with 112 additions and 117 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue