|
1 | | -import { useCallback, useMemo, useRef, useState } from 'react' |
| 1 | +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' |
2 | 2 | import { useChat } from 'ai/react' |
3 | 3 | import { MessageSquarePlus } from 'lucide-react' |
4 | 4 | import { useModel } from '../contexts/ModelContext' |
@@ -61,6 +61,7 @@ const TIMEOUT_ERROR_MESSAGE = |
61 | 61 | export function Chat() { |
62 | 62 | const hasMounted = useHasMounted() |
63 | 63 | const messagesEndRef = useRef<HTMLDivElement>(null) |
| 64 | + const seenMessageIds = useRef(new Set<string>()) |
64 | 65 | const [hasStartedChat, setHasStartedChat] = useState(false) |
65 | 66 | const [focusTimestamp, setFocusTimestamp] = useState(Date.now()) |
66 | 67 | const [servers, setServers] = useState<Servers>({}) |
@@ -127,16 +128,50 @@ export function Chat() { |
127 | 128 | onResponse: handleResponse, |
128 | 129 | }) |
129 | 130 |
|
| 131 | + useEffect(() => { |
| 132 | + if (!streaming && streamBuffer.length > 0) { |
| 133 | + // Extract only assistant messages from streamBuffer to sync with useChat messages |
| 134 | + // (user messages are already in messages via append) |
| 135 | + const assistantEvents = streamBuffer.filter( |
| 136 | + (event): event is Extract<StreamEvent, { type: 'assistant' }> => |
| 137 | + event.type === 'assistant', |
| 138 | + ) |
| 139 | + |
| 140 | + if (assistantEvents.length > 0) { |
| 141 | + setMessages((prevMessages) => { |
| 142 | + // Convert assistant stream events to Message objects, filtering duplicates using ref |
| 143 | + const newMessages = assistantEvents |
| 144 | + .filter((event) => !seenMessageIds.current.has(event.id)) |
| 145 | + .map((event) => ({ |
| 146 | + id: event.id, |
| 147 | + role: 'assistant' as const, |
| 148 | + content: event.content, |
| 149 | + })) |
| 150 | + |
| 151 | + // Track newly added message IDs |
| 152 | + newMessages.forEach((msg) => seenMessageIds.current.add(msg.id)) |
| 153 | + |
| 154 | + return [...prevMessages, ...newMessages] |
| 155 | + }) |
| 156 | + } |
| 157 | + |
| 158 | + // DO NOT clear the buffer here - we need to preserve all events for rendering |
| 159 | + // The buffer contains tool calls, code interpreter events, etc. that need to be displayed |
| 160 | + } |
| 161 | + }, [streaming, streamBuffer, setMessages]) |
| 162 | + |
130 | 163 | const renderEvents = useMemo<Array<StreamEvent | Message>>(() => { |
131 | | - if (streaming || streamBuffer.length > 0) { |
132 | | - return [...streamBuffer] |
| 164 | + // If we have streamBuffer content, render it directly (contains all events in order) |
| 165 | + if (streamBuffer.length > 0) { |
| 166 | + return streamBuffer |
133 | 167 | } |
134 | 168 | // Show initial message only when there are no real messages and no streaming |
135 | 169 | if (messages.length === 0 && !hasStartedChat) { |
136 | 170 | return [initialMessage] |
137 | 171 | } |
| 172 | + // After buffer is cleared (new chat), render from messages |
138 | 173 | return messages |
139 | | - }, [streaming, streamBuffer, messages, hasStartedChat, initialMessage]) |
| 174 | + }, [streamBuffer, messages, hasStartedChat, initialMessage]) |
140 | 175 |
|
141 | 176 | const handleSendMessage = useCallback( |
142 | 177 | (prompt: string) => { |
@@ -164,6 +199,7 @@ export function Chat() { |
164 | 199 | setHasStartedChat(false) |
165 | 200 | clearBuffer() |
166 | 201 | setMessages([]) |
| 202 | + seenMessageIds.current.clear() |
167 | 203 | setFocusTimestamp(Date.now()) |
168 | 204 | setUseCodeInterpreter(false) |
169 | 205 | setUseWebSearch(false) |
|
0 commit comments