fix: load historical messages when switching threads via client.threads.getState
This commit is contained in:
+35
-11
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user