@@ -23,6 +23,47 @@ function getStreamingModelUid(inputMessageUuid: string) { return `streaming:${inputMessageUuid}` } +/** Keeps first occurrence of each uid (chronological lists). */ +function dedupeMessagesByUid(messages: Message[]): Message[] { + const seen = new Set() + const result: Message[] = [] + + for (const message of messages) { + if (!message.uid || seen.has(message.uid)) { + continue + } + seen.add(message.uid) + result.push(message) + } + + return result +} + +/** + * Prepend older messages and drop duplicates by uid. + * Also drops local streaming placeholders when the real server copy of the pair arrives. + */ +function prependUniqueMessages(prev: Message[], incoming: Message[]): Message[] { + const existingUids = new Set(prev.map((message) => message.uid)) + + const toPrepend = incoming.filter((message) => !existingUids.has(message.uid)) + if (toPrepend.length === 0) { + return prev + } + + const incomingUids = new Set(incoming.map((message) => message.uid)) + const withoutResolvedStreaming = prev.filter((message) => { + if (!message.uid.startsWith('streaming:')) { + return true + } + const inputUuid = message.uid.slice('streaming:'.length) + // User message from this stream is already on the server page → drop placeholder model bubble + return !incomingUids.has(inputUuid) + }) + + return dedupeMessagesByUid([...toPrepend, ...withoutResolvedStreaming]) +} + function removeStreamingModelMessage(messages: Message[] | null, inputMessageUuid?: string | null) { const uidsToRemove = new Set([getStreamingModelUid(STREAMING_PENDING_UID)]) @@ -227,6 +268,7 @@ export function useChatModel( const [loading, setLoading] = useState(false) const [paginationLoading, setPaginationLoading] = useState(false) const [offset, setOffset] = useState(0) + const paginationLockRef = useRef(false) const abortRef = useRef(null) const isSendingRef = useRef(false) @@ -637,10 +679,11 @@ export function useChatModel( }, [currentChat, streaming, data?.access, tryReconnectOnLoad]) const getMessagesPagination = useCallback(async () => { - if (!currentChat || isSendingRef.current || paginationLoading) { + if (!currentChat || isSendingRef.current || paginationLoading || paginationLockRef.current) { return } + paginationLockRef.current = true setPaginationLoading(true) try { @@ -650,17 +693,14 @@ export function useChatModel( return } - const newMessages = answer.reverse() + const newMessages = [...answer].reverse() setMessages((prev) => { - const existingUids = new Set(prev?.map((message) => message.uid) ?? []) - const toPrepend = newMessages.filter((message) => !existingUids.has(message.uid)) - - if (toPrepend.length === 0) { - return prev ?? null + if (!prev) { + return dedupeMessagesByUid(newMessages) } - return [...toPrepend, ...(prev ?? [])] + return prependUniqueMessages(prev, newMessages) }) setOffset((prev) => prev + answer.length) @@ -668,6 +708,7 @@ export function useChatModel( console.error('[useChatModel] getMessagesPagination: request failed', { chatUid: currentChat, error }) showMessage('Ошибка загрузки сообщений') } finally { + paginationLockRef.current = false setPaginationLoading(false) } }, [currentChat, data?.access, offset, paginationLoading, showMessage]) @@ -727,7 +768,9 @@ export function useChatModel( showMessage('Непредвиденная ошибка, попробуйте еще раз') } } else { - setMessages((prev) => [...(prev ?? []), ...(result as Message[])]) + const resultMessages = result as Message[] + setMessages((prev) => dedupeMessagesByUid([...(prev ?? []), ...resultMessages])) + setOffset((prev) => prev + resultMessages.length) } } catch (error) { console.error('[useChatModel] sendMessage: request failed', { chatUid: currentChat, error }) @@ -793,7 +836,7 @@ export function useChatModel( const inputUuid = inputMessageUuidRef.current ?? readChatStreamSession(currentChat)?.inputMessageUuid ?? STREAMING_PENDING_UID - await reconnectToStream( + const reconnected = await reconnectToStream( currentChat, inputUuid, streamRuntimeRef.current.lastOffset, @@ -801,6 +844,10 @@ export function useChatModel( streamRuntimeRef.current.modelContent, { silent: true } ) + + if (reconnected) { + setOffset((prev) => prev + 2) + } return } @@ -814,6 +861,8 @@ export function useChatModel( markOptimisticUserMessageFailed() setMessages((prev) => removeStreamingModelMessage(prev)) showMessage('Непредвиденная ошибка, попробуйте еще раз') + } else if (result.streamCompleted && !result.streamFailed) { + setOffset((prev) => prev + 2) } } catch (error) { if (abortController.signal.aborted) { @@ -823,9 +872,12 @@ export function useChatModel( const session = readChatStreamSession(currentChat) if (session) { const inputUuid = inputMessageUuidRef.current ?? session.inputMessageUuid - await reconnectToStream(currentChat, inputUuid, session.lastOffset, undefined, session.modelContent, { + const reconnected = await reconnectToStream(currentChat, inputUuid, session.lastOffset, undefined, session.modelContent, { silent: true, }) + if (reconnected) { + setOffset((prev) => prev + 2) + } return }