Skip to content

Commit 24e724e

Browse files
authored
fix(explorer): Fix optimistic messages (#98781)
Fix the bug where optimistic messages disappeared before the backend responded
1 parent 9c6495f commit 24e724e

File tree

1 file changed

+103
-6
lines changed

1 file changed

+103
-6
lines changed

static/app/views/seerExplorer/hooks/useSeerExplorer.tsx

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type SeerExplorerChatResponse = {
2626
run_id: number;
2727
};
2828

29-
const POLL_INTERVAL = 500; // Poll every 500ms
29+
const POLL_INTERVAL = 1000; // Poll every second
3030

3131
const makeSeerExplorerQueryKey = (orgSlug: string, runId?: number): ApiQueryKey => [
3232
`/organizations/${orgSlug}/seer/explorer-chat/${runId ? `${runId}/` : ''}`,
@@ -85,6 +85,10 @@ export const useSeerExplorer = () => {
8585
const [optimisticMessageIds, setOptimisticMessageIds] = useState<Set<string>>(
8686
new Set()
8787
);
88+
const [pendingOptimisticBlocks, setPendingOptimisticBlocks] = useState<{
89+
blocks: Block[];
90+
insertIndex: number;
91+
} | null>(null);
8892

8993
const {data: apiData, isPending} = useApiQuery<SeerExplorerResponse>(
9094
makeSeerExplorerQueryKey(orgSlug || '', currentRunId || undefined),
@@ -156,11 +160,60 @@ export const useSeerExplorer = () => {
156160
prev => new Set([...prev, userMessage.id, loadingMessage.id])
157161
);
158162

163+
// Keep a local copy of optimistic blocks so they persist across polls
164+
setPendingOptimisticBlocks({
165+
blocks: [userMessage, loadingMessage],
166+
insertIndex: calculatedInsertIndex,
167+
});
168+
159169
setApiQueryData<SeerExplorerResponse>(
160170
queryClient,
161171
makeSeerExplorerQueryKey(orgSlug, currentRunId),
162172
{session: updatedSession}
163173
);
174+
} else {
175+
// Handle optimistic UI for the first message (no run yet or no session in cache)
176+
const userMessage: Block = {
177+
id: `user-${timestamp}`,
178+
message: {
179+
role: 'user',
180+
content: query,
181+
},
182+
timestamp: new Date().toISOString(),
183+
loading: false,
184+
};
185+
186+
const loadingMessage: Block = {
187+
id: `loading-${timestamp}`,
188+
message: {
189+
role: 'assistant',
190+
content: 'Thinking...',
191+
},
192+
timestamp: new Date().toISOString(),
193+
loading: true,
194+
};
195+
196+
setOptimisticMessageIds(
197+
prev => new Set([...prev, userMessage.id, loadingMessage.id])
198+
);
199+
200+
setPendingOptimisticBlocks({
201+
blocks: [userMessage, loadingMessage],
202+
insertIndex: calculatedInsertIndex,
203+
});
204+
205+
const newSession: NonNullable<SeerExplorerResponse['session']> = {
206+
run_id: undefined,
207+
blocks: [userMessage, loadingMessage],
208+
status: 'processing',
209+
updated_at: new Date().toISOString(),
210+
};
211+
212+
setApiQueryData<SeerExplorerResponse>(
213+
queryClient,
214+
makeSeerExplorerQueryKey(orgSlug),
215+
{session: newSession}
216+
);
164217
}
165218

166219
try {
@@ -181,17 +234,13 @@ export const useSeerExplorer = () => {
181234
setCurrentRunId(response.run_id);
182235
}
183236

184-
// Clear deletedFromIndex since we just sent a message from that point
185-
if (deletedFromIndex !== null) {
186-
setDeletedFromIndex(null);
187-
}
188-
189237
// Invalidate queries to fetch fresh data
190238
queryClient.invalidateQueries({
191239
queryKey: makeSeerExplorerQueryKey(orgSlug, response.run_id),
192240
});
193241
} catch (e: any) {
194242
setWaitingForResponse(false);
243+
setPendingOptimisticBlocks(null);
195244
setApiQueryData<SeerExplorerResponse>(
196245
queryClient,
197246
makeSeerExplorerQueryKey(orgSlug, currentRunId || undefined),
@@ -207,6 +256,7 @@ export const useSeerExplorer = () => {
207256
setWaitingForResponse(false);
208257
setDeletedFromIndex(null);
209258
setOptimisticMessageIds(new Set());
259+
setPendingOptimisticBlocks(null);
210260
if (orgSlug) {
211261
setApiQueryData<SeerExplorerResponse>(
212262
queryClient,
@@ -222,6 +272,18 @@ export const useSeerExplorer = () => {
222272

223273
// Always filter messages based on deletedFromIndex before any other processing
224274
let sessionData = apiData?.session ?? null;
275+
276+
// If we are between queries (e.g., first message just set a new run id and
277+
// the new query hasn't returned yet), keep showing optimistic blocks by
278+
// constructing an ephemeral processing session.
279+
if (!sessionData && pendingOptimisticBlocks) {
280+
sessionData = {
281+
run_id: currentRunId ?? undefined,
282+
blocks: pendingOptimisticBlocks.blocks,
283+
status: 'processing',
284+
updated_at: new Date().toISOString(),
285+
};
286+
}
225287
if (sessionData?.blocks && deletedFromIndex !== null) {
226288
// Separate optimistic messages from real messages
227289
const optimisticMessages = sessionData.blocks.filter(msg =>
@@ -240,6 +302,39 @@ export const useSeerExplorer = () => {
240302
};
241303
}
242304

305+
// If we have pending optimistic blocks and the server has not completed processing,
306+
// ensure they remain visible even if the next poll hasn't included them yet.
307+
if (sessionData) {
308+
if (pendingOptimisticBlocks && sessionData.status === 'processing') {
309+
const existingIds = new Set(sessionData.blocks.map(b => b.id));
310+
const nonExistingOptimistic = pendingOptimisticBlocks.blocks.filter(
311+
b => !existingIds.has(b.id)
312+
);
313+
314+
if (nonExistingOptimistic.length > 0) {
315+
const safeInsertIndex = Math.min(
316+
Math.max(pendingOptimisticBlocks.insertIndex, 0),
317+
sessionData.blocks.length
318+
);
319+
const mergedBlocks = [
320+
...sessionData.blocks.slice(0, safeInsertIndex),
321+
...nonExistingOptimistic,
322+
...sessionData.blocks.slice(safeInsertIndex),
323+
];
324+
sessionData = {
325+
...sessionData,
326+
blocks: mergedBlocks,
327+
status: 'processing',
328+
};
329+
}
330+
}
331+
332+
// If processing is done, clear any pending optimistic blocks
333+
if (pendingOptimisticBlocks && sessionData.status !== 'processing') {
334+
setPendingOptimisticBlocks(null);
335+
}
336+
}
337+
243338
if (waitingForResponse && sessionData?.blocks) {
244339
// Stop waiting once we see the response is no longer loading
245340
const hasLoadingMessage = sessionData.blocks.some(block => block.loading);
@@ -250,6 +345,8 @@ export const useSeerExplorer = () => {
250345
setDeletedFromIndex(null);
251346
// Clear optimistic message IDs since they should now be real messages
252347
setOptimisticMessageIds(new Set());
348+
// Clear any pending optimistic blocks
349+
setPendingOptimisticBlocks(null);
253350
}
254351
}
255352

0 commit comments

Comments
 (0)