@@ -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
3131const 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