|
1 | 1 | import { useCallback, useEffect, useRef, useState } from 'react' |
2 | 2 | import { useQueryClient } from '@tanstack/react-query' |
3 | | -import { usePathname } from 'next/navigation' |
| 3 | +import { usePathname, useRouter } from 'next/navigation' |
4 | 4 | import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants' |
5 | 5 | import { |
| 6 | + type TaskChatHistory, |
6 | 7 | type TaskStoredContentBlock, |
7 | 8 | type TaskStoredMessage, |
8 | 9 | taskKeys, |
@@ -71,13 +72,22 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined { |
71 | 72 | export function useChat(workspaceId: string, initialChatId?: string): UseChatReturn { |
72 | 73 | const pathname = usePathname() |
73 | 74 | const queryClient = useQueryClient() |
| 75 | + const router = useRouter() |
| 76 | + const routerRef = useRef(router) |
74 | 77 | const [messages, setMessages] = useState<ChatMessage[]>([]) |
75 | 78 | const [isSending, setIsSending] = useState(false) |
76 | 79 | const [error, setError] = useState<string | null>(null) |
77 | 80 | const abortControllerRef = useRef<AbortController | null>(null) |
78 | 81 | const chatIdRef = useRef<string | undefined>(initialChatId) |
79 | 82 | const chatBottomRef = useRef<HTMLDivElement>(null) |
80 | 83 | const appliedChatIdRef = useRef<string | undefined>(undefined) |
| 84 | + const pendingUserMsgRef = useRef<{ id: string; content: string } | null>(null) |
| 85 | + const streamIdRef = useRef<string | undefined>(undefined) |
| 86 | + const sendingRef = useRef(false) |
| 87 | + |
| 88 | + useEffect(() => { |
| 89 | + routerRef.current = router |
| 90 | + }, [router]) |
81 | 91 |
|
82 | 92 | const isHomePage = pathname.endsWith('/home') |
83 | 93 |
|
@@ -161,11 +171,17 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet |
161 | 171 | chatIdRef.current = parsed.chatId |
162 | 172 | queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) |
163 | 173 | if (isNewChat) { |
164 | | - window.history.replaceState( |
165 | | - null, |
166 | | - '', |
167 | | - `/workspace/${workspaceId}/task/${parsed.chatId}` |
168 | | - ) |
| 174 | + const userMsg = pendingUserMsgRef.current |
| 175 | + const activeStreamId = streamIdRef.current |
| 176 | + if (userMsg && activeStreamId) { |
| 177 | + queryClient.setQueryData<TaskChatHistory>(taskKeys.detail(parsed.chatId), { |
| 178 | + id: parsed.chatId, |
| 179 | + title: null, |
| 180 | + messages: [{ id: userMsg.id, role: 'user', content: userMsg.content }], |
| 181 | + activeStreamId, |
| 182 | + }) |
| 183 | + } |
| 184 | + routerRef.current.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`) |
169 | 185 | } |
170 | 186 | } |
171 | 187 | break |
@@ -266,7 +282,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet |
266 | 282 |
|
267 | 283 | useEffect(() => { |
268 | 284 | const activeStreamId = chatHistory?.activeStreamId |
269 | | - if (!activeStreamId || !appliedChatIdRef.current) return |
| 285 | + if (!activeStreamId || !appliedChatIdRef.current || sendingRef.current) return |
270 | 286 |
|
271 | 287 | const abortController = new AbortController() |
272 | 288 | abortControllerRef.current = abortController |
@@ -295,19 +311,41 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet |
295 | 311 |
|
296 | 312 | return () => { |
297 | 313 | abortController.abort() |
| 314 | + appliedChatIdRef.current = undefined |
298 | 315 | } |
299 | 316 | }, [chatHistory?.activeStreamId, processSSEStream, finalize]) |
300 | 317 |
|
301 | 318 | const sendMessage = useCallback( |
302 | 319 | async (message: string) => { |
303 | 320 | if (!message.trim() || !workspaceId) return |
304 | 321 |
|
| 322 | + abortControllerRef.current?.abort() |
| 323 | + |
305 | 324 | setError(null) |
306 | 325 | setIsSending(true) |
| 326 | + sendingRef.current = true |
307 | 327 |
|
308 | 328 | const userMessageId = crypto.randomUUID() |
309 | 329 | const assistantId = crypto.randomUUID() |
310 | 330 |
|
| 331 | + pendingUserMsgRef.current = { id: userMessageId, content: message } |
| 332 | + streamIdRef.current = userMessageId |
| 333 | + |
| 334 | + if (chatIdRef.current) { |
| 335 | + queryClient.setQueryData<TaskChatHistory>(taskKeys.detail(chatIdRef.current), (old) => |
| 336 | + old |
| 337 | + ? { |
| 338 | + ...old, |
| 339 | + messages: [ |
| 340 | + ...old.messages, |
| 341 | + { id: userMessageId, role: 'user' as const, content: message }, |
| 342 | + ], |
| 343 | + activeStreamId: userMessageId, |
| 344 | + } |
| 345 | + : undefined |
| 346 | + ) |
| 347 | + } |
| 348 | + |
311 | 349 | setMessages((prev) => [ |
312 | 350 | ...prev, |
313 | 351 | { id: userMessageId, role: 'user', content: message }, |
@@ -343,10 +381,11 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet |
343 | 381 | if (err instanceof Error && err.name === 'AbortError') return |
344 | 382 | setError(err instanceof Error ? err.message : 'Failed to send message') |
345 | 383 | } finally { |
| 384 | + sendingRef.current = false |
346 | 385 | finalize() |
347 | 386 | } |
348 | 387 | }, |
349 | | - [workspaceId, processSSEStream, finalize] |
| 388 | + [workspaceId, queryClient, processSSEStream, finalize] |
350 | 389 | ) |
351 | 390 |
|
352 | 391 | const stopGeneration = useCallback(() => { |
|
0 commit comments