diff --git a/langgraph/src/main.tsx b/langgraph/src/main.tsx index 5bab780..a2aca46 100644 --- a/langgraph/src/main.tsx +++ b/langgraph/src/main.tsx @@ -112,6 +112,10 @@ function App() { const [currentThreadId, setCurrentThreadId] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(false); + // Historical messages fetched when switching to an existing thread + const [historicalMessages, setHistoricalMessages] = useState([]); + const [historicalUi, setHistoricalUi] = useState([]); + // Canvas panel state const [canvasDoc, setCanvasDoc] = useState(null); @@ -243,7 +247,7 @@ function App() { setSidebarOpen(false); }, [currentThreadId, input]); - const handleSelectThread = useCallback((threadId: string) => { + const handleSelectThread = useCallback(async (threadId: string) => { // Save draft for current thread if (currentThreadId) { try { localStorage.setItem(`draft_${currentThreadId}`, input); } catch { /* ignore */ } @@ -255,8 +259,22 @@ function App() { } catch { setInput(""); } + // Clear historical messages before switching + setHistoricalMessages([]); + setHistoricalUi([]); setCurrentThreadId(threadId); setSidebarOpen(false); + // Fetch existing thread state so history is visible immediately + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const state = await (client.threads as any).getState(threadId); + if (state?.values?.messages) { + setHistoricalMessages(state.values.messages as Message[]); + } + if (state?.values?.ui) { + setHistoricalUi(state.values.ui as UIMsgLocal[]); + } + } catch { /* graceful degradation */ } }, [currentThreadId, input]); const handleDeleteThread = useCallback(async (threadId: string) => { @@ -344,7 +362,7 @@ function App() { } // Auto-name thread on first message - const isFirstMessage = thread.messages.length === 0; + const isFirstMessage = activeMessages.length === 0; // Consume pending retry tool hint (set by soc:retry-tool event) const retryTool = pendingRetryToolRef.current; @@ -418,7 +436,7 @@ function App() { // ── Regenerate last AI message ─────────────────────────────────────────── function handleRegenerate() { // Find last human message - const humanMsgs = thread.messages.filter((m) => m.type === "human"); + const humanMsgs = activeMessages.filter((m) => m.type === "human"); const lastHuman = humanMsgs[humanMsgs.length - 1]; if (!lastHuman || thread.isLoading) return; @@ -471,9 +489,12 @@ function App() { reader.readAsDataURL(file); } + // ── Active messages: prefer streaming messages, fall back to historical ── + const activeMessages: Message[] = thread.messages.length > 0 ? thread.messages : historicalMessages; + // ── Collect completed tool call IDs ───────────────────────────────────── const completedToolIds = new Set( - thread.messages + activeMessages .filter((m) => m.type === "tool") .map((m) => (m as any).tool_call_id as string) .filter(Boolean), @@ -481,7 +502,7 @@ function App() { // ── Collect failed tool call IDs ───────────────────────────────────────── const failedToolIds = new Set( - thread.messages + activeMessages .filter((m) => m.type === "tool" && (m as any).status === "error") .map((m) => (m as any).tool_call_id as string) .filter(Boolean), @@ -500,7 +521,7 @@ function App() { }>; // ── Find last AI message index ────────────────────────────────────────── - const lastAiIdx = thread.messages.reduce((last, m, i) => (m.type === "ai" ? i : last), -1); + const lastAiIdx = activeMessages.reduce((last, m, i) => (m.type === "ai" ? i : last), -1); return (
@@ -566,7 +587,7 @@ function App() { setShowScrollBtn(el.scrollHeight - el.scrollTop - el.clientHeight > 200); }} > - {thread.messages.length === 0 && ( + {activeMessages.length === 0 && (

你好,有什么可以帮你的?

@@ -589,11 +610,14 @@ function App() {
)} - {thread.messages.map((message, idx) => { + {activeMessages.map((message, idx) => { // Render UI cards attached to this message // eslint-disable-next-line @typescript-eslint/no-explicit-any + const allUi: UIMsgLocal[] = thread.messages.length > 0 + ? ((thread.values as any)?.ui ?? []) + : historicalUi; const uiItemsRaw = deduplicateUiItems( - ((thread.values as any)?.ui ?? []).filter( + allUi.filter( (ui: UIMsgLocal) => ui.metadata?.message_id === message.id, ) as UIMsgLocal[] ); @@ -779,7 +803,7 @@ function App() { {/* Loading placeholder: show 3-dot bounce when waiting for first AI tokens after a human message */} {(() => { - const lastMsg = thread.messages[thread.messages.length - 1]; + const lastMsg = activeMessages[activeMessages.length - 1]; const showLoadingDots = thread.isLoading && lastMsg?.type === "human"; return showLoadingDots ? (
@@ -794,7 +818,7 @@ function App() { {thread.isLoading && deduplicateUiItems( (thread.values?.ui ?? []).filter((ui: UIMsgLocal) => { - const attachedToExisting = thread.messages.some( + const attachedToExisting = activeMessages.some( (m) => m.id === ui.metadata?.message_id, ); return !attachedToExisting;