fix: load historical messages when switching threads via client.threads.getState
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 29s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 47s

This commit is contained in:
gongzhiyong
2026-04-12 15:14:29 +08:00
parent 0d36c0fc0d
commit 964e0dd305
+35 -11
View File
@@ -112,6 +112,10 @@ function App() {
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null); const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
// Historical messages fetched when switching to an existing thread
const [historicalMessages, setHistoricalMessages] = useState<Message[]>([]);
const [historicalUi, setHistoricalUi] = useState<UIMsgLocal[]>([]);
// Canvas panel state // Canvas panel state
const [canvasDoc, setCanvasDoc] = useState<CanvasDoc | null>(null); const [canvasDoc, setCanvasDoc] = useState<CanvasDoc | null>(null);
@@ -243,7 +247,7 @@ function App() {
setSidebarOpen(false); setSidebarOpen(false);
}, [currentThreadId, input]); }, [currentThreadId, input]);
const handleSelectThread = useCallback((threadId: string) => { const handleSelectThread = useCallback(async (threadId: string) => {
// Save draft for current thread // Save draft for current thread
if (currentThreadId) { if (currentThreadId) {
try { localStorage.setItem(`draft_${currentThreadId}`, input); } catch { /* ignore */ } try { localStorage.setItem(`draft_${currentThreadId}`, input); } catch { /* ignore */ }
@@ -255,8 +259,22 @@ function App() {
} catch { } catch {
setInput(""); setInput("");
} }
// Clear historical messages before switching
setHistoricalMessages([]);
setHistoricalUi([]);
setCurrentThreadId(threadId); setCurrentThreadId(threadId);
setSidebarOpen(false); 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]); }, [currentThreadId, input]);
const handleDeleteThread = useCallback(async (threadId: string) => { const handleDeleteThread = useCallback(async (threadId: string) => {
@@ -344,7 +362,7 @@ function App() {
} }
// Auto-name thread on first message // 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) // Consume pending retry tool hint (set by soc:retry-tool event)
const retryTool = pendingRetryToolRef.current; const retryTool = pendingRetryToolRef.current;
@@ -418,7 +436,7 @@ function App() {
// ── Regenerate last AI message ─────────────────────────────────────────── // ── Regenerate last AI message ───────────────────────────────────────────
function handleRegenerate() { function handleRegenerate() {
// Find last human message // 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]; const lastHuman = humanMsgs[humanMsgs.length - 1];
if (!lastHuman || thread.isLoading) return; if (!lastHuman || thread.isLoading) return;
@@ -471,9 +489,12 @@ function App() {
reader.readAsDataURL(file); 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 ───────────────────────────────────── // ── Collect completed tool call IDs ─────────────────────────────────────
const completedToolIds = new Set( const completedToolIds = new Set(
thread.messages activeMessages
.filter((m) => m.type === "tool") .filter((m) => m.type === "tool")
.map((m) => (m as any).tool_call_id as string) .map((m) => (m as any).tool_call_id as string)
.filter(Boolean), .filter(Boolean),
@@ -481,7 +502,7 @@ function App() {
// ── Collect failed tool call IDs ───────────────────────────────────────── // ── Collect failed tool call IDs ─────────────────────────────────────────
const failedToolIds = new Set( const failedToolIds = new Set(
thread.messages activeMessages
.filter((m) => m.type === "tool" && (m as any).status === "error") .filter((m) => m.type === "tool" && (m as any).status === "error")
.map((m) => (m as any).tool_call_id as string) .map((m) => (m as any).tool_call_id as string)
.filter(Boolean), .filter(Boolean),
@@ -500,7 +521,7 @@ function App() {
}>; }>;
// ── Find last AI message index ────────────────────────────────────────── // ── 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 ( return (
<div className="h-screen flex flex-row bg-background text-foreground overflow-hidden"> <div className="h-screen flex flex-row bg-background text-foreground overflow-hidden">
@@ -566,7 +587,7 @@ function App() {
setShowScrollBtn(el.scrollHeight - el.scrollTop - el.clientHeight > 200); setShowScrollBtn(el.scrollHeight - el.scrollTop - el.clientHeight > 200);
}} }}
> >
{thread.messages.length === 0 && ( {activeMessages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground select-none"> <div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground select-none">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<p className="text-lg font-medium">你好,有什么可以帮你的?</p> <p className="text-lg font-medium">你好,有什么可以帮你的?</p>
@@ -589,11 +610,14 @@ function App() {
</div> </div>
)} )}
{thread.messages.map((message, idx) => { {activeMessages.map((message, idx) => {
// Render UI cards attached to this message // Render UI cards attached to this message
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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( const uiItemsRaw = deduplicateUiItems(
((thread.values as any)?.ui ?? []).filter( allUi.filter(
(ui: UIMsgLocal) => ui.metadata?.message_id === message.id, (ui: UIMsgLocal) => ui.metadata?.message_id === message.id,
) as UIMsgLocal[] ) as UIMsgLocal[]
); );
@@ -779,7 +803,7 @@ function App() {
{/* Loading placeholder: show 3-dot bounce when waiting for first AI tokens after a human message */} {/* 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"; const showLoadingDots = thread.isLoading && lastMsg?.type === "human";
return showLoadingDots ? ( return showLoadingDots ? (
<div className="flex items-center gap-1.5 px-4 py-3 rounded-2xl rounded-bl-sm bg-muted text-muted-foreground w-fit"> <div className="flex items-center gap-1.5 px-4 py-3 rounded-2xl rounded-bl-sm bg-muted text-muted-foreground w-fit">
@@ -794,7 +818,7 @@ function App() {
{thread.isLoading && {thread.isLoading &&
deduplicateUiItems( deduplicateUiItems(
(thread.values?.ui ?? []).filter((ui: UIMsgLocal) => { (thread.values?.ui ?? []).filter((ui: UIMsgLocal) => {
const attachedToExisting = thread.messages.some( const attachedToExisting = activeMessages.some(
(m) => m.id === ui.metadata?.message_id, (m) => m.id === ui.metadata?.message_id,
); );
return !attachedToExisting; return !attachedToExisting;