fix(web): guard restored chat history hydration

This commit is contained in:
XYSK-lilong007 2026-03-12 19:13:58 +08:00
parent 2b25f7fb48
commit 16ce1a80bb

View file

@ -1,6 +1,12 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import { useAtomValue } from "jotai" import { useAtomValue } from "jotai"
import { useCallback, useEffect, useRef, useState } from "react" import {
type SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@ -131,7 +137,27 @@ export function usePicoChat() {
const isConnectingRef = useRef(false) const isConnectingRef = useRef(false)
const msgIdCounter = useRef(0) const msgIdCounter = useRef(0)
const activeSessionIdRef = useRef(activeSessionId) const activeSessionIdRef = useRef(activeSessionId)
const hydratedInitialSessionRef = useRef(false) const messagesRevisionRef = useRef(0)
const setTrackedMessages = useCallback(
(nextState: SetStateAction<ChatMessage[]>) => {
setMessages((prev) => {
const next =
typeof nextState === "function"
? (
nextState as (prevState: ChatMessage[]) => ChatMessage[]
)(prev)
: nextState
if (next !== prev) {
messagesRevisionRef.current += 1
}
return next
})
},
[],
)
// Keep ref in sync // Keep ref in sync
useEffect(() => { useEffect(() => {
@ -152,23 +178,25 @@ export function usePicoChat() {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (hydratedInitialSessionRef.current) {
return
}
hydratedInitialSessionRef.current = true
const storedSessionId = readStoredSessionId() const storedSessionId = readStoredSessionId()
if (!storedSessionId) { if (!storedSessionId) {
return return
} }
const restoreRevision = messagesRevisionRef.current
let cancelled = false let cancelled = false
void loadSessionMessages(storedSessionId) void loadSessionMessages(storedSessionId)
.then((historyMessages) => { .then((historyMessages) => {
if (cancelled) { if (cancelled) {
return return
} }
setMessages(historyMessages) if (activeSessionIdRef.current !== storedSessionId) {
return
}
if (messagesRevisionRef.current !== restoreRevision) {
return
}
setTrackedMessages(historyMessages)
setIsTyping(false) setIsTyping(false)
}) })
.catch((err) => { .catch((err) => {
@ -176,15 +204,21 @@ export function usePicoChat() {
if (cancelled) { if (cancelled) {
return return
} }
if (activeSessionIdRef.current !== storedSessionId) {
return
}
if (messagesRevisionRef.current !== restoreRevision) {
return
}
localStorage.removeItem(LAST_SESSION_STORAGE_KEY) localStorage.removeItem(LAST_SESSION_STORAGE_KEY)
setMessages([]) setTrackedMessages([])
setIsTyping(false) setIsTyping(false)
}) })
return () => { return () => {
cancelled = true cancelled = true
} }
}, [loadSessionMessages]) }, [loadSessionMessages, setTrackedMessages])
const handlePicoMessage = useCallback((msg: PicoMessage) => { const handlePicoMessage = useCallback((msg: PicoMessage) => {
const payload = msg.payload || {} const payload = msg.payload || {}
@ -199,7 +233,7 @@ export function usePicoChat() {
? normalizeUnixTimestamp(Number(msg.timestamp)) ? normalizeUnixTimestamp(Number(msg.timestamp))
: Date.now() : Date.now()
setMessages((prev) => [ setTrackedMessages((prev) => [
...prev, ...prev,
{ {
id: messageId, id: messageId,
@ -217,7 +251,7 @@ export function usePicoChat() {
const messageId = payload.message_id as string const messageId = payload.message_id as string
if (!messageId) break if (!messageId) break
setMessages((prev) => setTrackedMessages((prev) =>
prev.map((m) => (m.id === messageId ? { ...m, content } : m)), prev.map((m) => (m.id === messageId ? { ...m, content } : m)),
) )
break break
@ -243,7 +277,7 @@ export function usePicoChat() {
default: default:
console.log("Unknown pico message type:", msg.type) console.log("Unknown pico message type:", msg.type)
} }
}, []) }, [setTrackedMessages])
const connect = useCallback(async () => { const connect = useCallback(async () => {
if ( if (
@ -365,7 +399,7 @@ export function usePicoChat() {
const timestampRaw = Date.now() const timestampRaw = Date.now()
// Add user message to local state // Add user message to local state
setMessages((prev) => [ setTrackedMessages((prev) => [
...prev, ...prev,
{ id, role: "user", content, timestamp: timestampRaw }, { id, role: "user", content, timestamp: timestampRaw },
]) ])
@ -380,7 +414,7 @@ export function usePicoChat() {
payload: { content }, payload: { content },
} }
wsRef.current.send(JSON.stringify(picoMsg)) wsRef.current.send(JSON.stringify(picoMsg))
}, []) }, [setTrackedMessages])
// Switch to a historical session // Switch to a historical session
const switchSession = useCallback( const switchSession = useCallback(
@ -396,7 +430,7 @@ export function usePicoChat() {
disconnect() disconnect()
setActiveSessionId(sessionId) setActiveSessionId(sessionId)
setIsTyping(false) setIsTyping(false)
setMessages(historyMessages) setTrackedMessages(historyMessages)
} catch (err) { } catch (err) {
console.error("Failed to load session history:", err) console.error("Failed to load session history:", err)
toast.error(t("chat.historyOpenFailed")) toast.error(t("chat.historyOpenFailed"))
@ -409,7 +443,7 @@ export function usePicoChat() {
} }
}, 100) }, 100)
}, },
[connect, disconnect, gatewayState, loadSessionMessages, t], [connect, disconnect, gatewayState, loadSessionMessages, setTrackedMessages, t],
) )
// Start a new empty chat // Start a new empty chat
@ -421,7 +455,7 @@ export function usePicoChat() {
disconnect() disconnect()
const newId = generateSessionId() const newId = generateSessionId()
setActiveSessionId(newId) setActiveSessionId(newId)
setMessages([]) setTrackedMessages([])
setIsTyping(false) setIsTyping(false)
// Reconnect with the fresh session // Reconnect with the fresh session
@ -430,7 +464,7 @@ export function usePicoChat() {
connect() connect()
} }
}, 100) }, 100)
}, [disconnect, connect, gatewayState, messages.length]) }, [disconnect, connect, gatewayState, messages.length, setTrackedMessages])
return { return {
messages, messages,