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";