File size: 6,567 Bytes
12621bc 620331c 12621bc 620331c 12621bc 620331c 12621bc 620331c 12621bc |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
import React from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useParams } from "react-router-dom";
import { isHumanMessage, isAIMessage, AIMessage } from "@langchain/core/messages";
import { MessagesProps } from "../types";
import { HumanMessageComponent } from "./HumanMessage";
import { AIMessageComponent } from "./AIMessage";
import { Button } from "@/components/ui/button";
import { ArrowDown } from "lucide-react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
export const Messages = React.memo(({
messages,
streamingHumanMessage,
streamingAIMessageChunks,
setPreviewDocument,
onEditMessage,
onRegenerateMessage,
editingMessageIndex,
onSaveEdit,
onCancelEdit,
}: MessagesProps) => {
const { id } = useParams();
const viewportRef = React.useRef<HTMLDivElement>(null);
const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);
const initialLoadRef = React.useRef(true);
const handleScroll = React.useCallback((event: Event) => {
const viewport = event.target as HTMLDivElement;
const isNotAtBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight > 10;
setShowScrollToBottom(isNotAtBottom);
}, []);
const scrollToBottom = React.useCallback(() => {
if (!viewportRef.current) return;
const viewport = viewportRef.current;
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: 'smooth'
});
}, []);
// Effect for handling scroll events
React.useEffect(() => {
const viewport = viewportRef.current;
if (!viewport) return;
viewport.addEventListener('scroll', handleScroll);
// Initial check for scroll position
handleScroll({ target: viewport } as unknown as Event);
// Check scroll position after a short delay to account for content rendering
const checkTimeout = setTimeout(() => {
handleScroll({ target: viewport } as unknown as Event);
}, 100);
return () => {
viewport.removeEventListener('scroll', handleScroll);
clearTimeout(checkTimeout);
};
}, [handleScroll, messages, streamingAIMessageChunks]);
// Effect for initial scroll to bottom on page load
React.useEffect(() => {
if (initialLoadRef.current && messages && messages.length > 0 && viewportRef.current) {
// Use a timeout to ensure content is rendered before scrolling
const initialScrollTimeout = setTimeout(() => {
if (viewportRef.current) {
viewportRef.current.scrollTo({
top: viewportRef.current.scrollHeight,
behavior: 'smooth'
});
initialLoadRef.current = false;
}
}, 100);
return () => clearTimeout(initialScrollTimeout);
}
}, [messages]);
// Reset initialLoadRef when chat ID changes
React.useEffect(() => {
// Reset the initial load flag when the chat ID changes
initialLoadRef.current = true;
// Attempt to scroll to bottom after a short delay
if (id && id !== "new") {
const resetScrollTimeout = setTimeout(() => {
if (viewportRef.current && messages && messages.length > 0) {
viewportRef.current.scrollTo({
top: viewportRef.current.scrollHeight,
behavior: 'smooth'
});
}
}, 200);
return () => clearTimeout(resetScrollTimeout);
}
}, [id]);
// Scroll to bottom when streaming messages change
React.useEffect(() => {
// Only auto-scroll if we're already near the bottom or if this is the first message chunk
if (viewportRef.current && streamingAIMessageChunks.length > 0) {
const viewport = viewportRef.current;
const isNearBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 100;
if (isNearBottom || streamingAIMessageChunks.length === 1) {
// Use requestAnimationFrame to ensure smooth scrolling during streaming
requestAnimationFrame(() => {
if (viewportRef.current) {
viewportRef.current.scrollTo({
top: viewportRef.current.scrollHeight,
behavior: streamingAIMessageChunks.length === 1 ? 'smooth' : 'auto'
});
}
});
}
}
}, [streamingAIMessageChunks]);
if (id === "new" || !messages) {
return <div className="flex-1 min-h-0"><ScrollArea className="h-full" /></div>;
}
return (
<div className="flex-1 min-h-0 relative">
<ScrollAreaPrimitive.Root className="h-full">
<ScrollAreaPrimitive.Viewport ref={viewportRef} className="h-full w-full">
<div className="flex flex-col w-1/2 mx-auto gap-1 pb-4">
{messages.map((message, index) => {
if (isHumanMessage(message)) {
return (
<HumanMessageComponent
key={index}
message={message}
setPreviewDocument={setPreviewDocument}
onEdit={() => onEditMessage(index)}
onRegenerate={() => onRegenerateMessage(index)}
isEditing={editingMessageIndex === index}
onSave={onSaveEdit}
onCancelEdit={onCancelEdit}
/>
);
}
if (isAIMessage(message)) {
return <AIMessageComponent key={index} message={message} />;
}
return null;
})}
{streamingHumanMessage && (
<HumanMessageComponent
message={streamingHumanMessage}
setPreviewDocument={setPreviewDocument}
/>
)}
{streamingAIMessageChunks.length > 0 && (
<AIMessageComponent
message={new AIMessage(streamingAIMessageChunks.map(chunk => chunk.content).join(""))}
/>
)}
</div>
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Scrollbar orientation="vertical">
<ScrollAreaPrimitive.Thumb />
</ScrollAreaPrimitive.Scrollbar>
</ScrollAreaPrimitive.Root>
{showScrollToBottom && (
<Button
variant="secondary"
size="icon"
className="absolute z-50 bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-md hover:bg-accent bg-background/80 backdrop-blur-sm"
onClick={scrollToBottom}
>
<ArrowDown className="h-4 w-4" />
</Button>
)}
</div>
);
});
Messages.displayName = "Messages"; |