22<script setup lang="ts">
33import type { Doc , Id } from ' backend-convex/convex/_generated/dataModel'
44import type Lenis from ' lenis'
5- import { keyBy , randomStr , sleep , uniquePromise } from ' @namesmt/utils'
5+ import { keyBy , objectPick , randomStr , sleep , uniquePromise } from ' @namesmt/utils'
66import { api } from ' backend-convex/convex/_generated/api'
77import { useConvexClient } from ' convex-vue'
88import { countdown , debounce , getInstance , throttle } from ' kontroll'
@@ -43,7 +43,7 @@ const cachedThreadsMessages: {
4343} = {}
4444const messages = ref <Array <CustomMessage >>([])
4545const messagesKeyed = computed (() => keyBy (messages .value , ' id' ))
46- const streamingMessages = ref ( 0 )
46+ const streamingMessagesMap = reactive < Record < string , true >>({ } )
4747const isFetching = ref (false )
4848const chatInput = ref (' ' )
4949
@@ -56,14 +56,25 @@ const { ignoreUpdates: ignorePathUpdate } = watchIgnorable(
5656
5757 messages .value = cachedThreadsMessages [threadId as string ] ?? []
5858
59- doScrollBottom ({ smooth: false })
59+ nextTick (() => { doScrollBottom ({ smooth: false , maybe: true }) })
6060
6161 if (threadId ) {
6262 isFetching .value = true
6363 await convex .query (api .messages .listByThread , { threadId: threadId as Doc <' threads' >[' _id' ], lockerKey: getLockerKey (threadId ) })
64- .then ((existingMessages ) => {
64+ .then ((messagesFromConvex ) => {
6565 if (threadIdRef .value === threadId ) {
66- messages .value = existingMessages .map (customMessageTransform )
66+ if (threadId === oldThreadId ) {
67+ for (const index in messagesFromConvex ) {
68+ const m = messagesFromConvex [index ]!
69+ if (m .streamId && ! streamingMessagesMap [m .streamId ]) {
70+ messages .value .push (customMessageTransform (messagesFromConvex [+ index - 1 ]! ))
71+ messages .value .push (customMessageTransform (m ))
72+ }
73+ }
74+ }
75+ else {
76+ messages .value = messagesFromConvex .map (customMessageTransform )
77+ }
6778 }
6879 })
6980 .catch ((e ) => {
@@ -93,11 +104,12 @@ const { ignoreUpdates: ignorePathUpdate } = watchIgnorable(
93104 && message .streamId
94105 ) {
95106 console .log (' Attempting to resume stream for session:' , message .streamId )
96- uniquePromise (message .streamId , () => resumeStreamProcess (message .streamId ! , message .id ))
107+ nextTick (() => { uniquePromise (message .streamId ! , () => resumeStreamProcess (message .streamId ! , message .id )) } )
97108 }
98109 }
99110
100- nextTick (() => doScrollBottom ({ tries: 6 }))
111+ if (threadId !== oldThreadId )
112+ nextTick (() => doScrollBottom ({ tries: 6 }))
101113 }
102114 },
103115 { immediate: true },
@@ -140,6 +152,8 @@ async function handleSubmit({ input }: HandleSubmitArgs) {
140152 .then (() => { sleep (500 ).then (() => handleSubmit ({ input })) })
141153 }
142154
155+ const streamId = ` stream-${Date .now ()}_${randomStr (4 )} `
156+
143157 // Optimistically add the messages
144158 messages .value .push ({
145159 id: ` user-${Date .now ()}_${randomStr (4 )} ` ,
@@ -153,7 +167,7 @@ async function handleSubmit({ input }: HandleSubmitArgs) {
153167 model: chatContext .activeAgent .value .model ,
154168 content: ' ' ,
155169 isStreaming: true ,
156- streamId: undefined ,
170+ streamId ,
157171 } as any as CustomMessage )
158172
159173 // For some reason creating object reference first does not work, so we push and then get last message
@@ -187,8 +201,8 @@ async function handleSubmit({ input }: HandleSubmitArgs) {
187201 // Wraps in a kontroller to make sure there is only one stream on the same message
188202 throttle (
189203 1 ,
190- () => streamToMessage ({ message: targetMessage , content: userInput }),
191- { key: ` messageStream-${targetMessage . id } ` },
204+ () => streamToMessage ({ message: targetMessage , content: userInput , streamId }),
205+ { key: ` messageStream-${streamId } ` },
192206 )
193207}
194208
@@ -200,10 +214,10 @@ async function resumeStreamProcess(streamSessionId: string, messageId: string) {
200214 if (getInstance (threadIdRef .value ))
201215 return console .warn (' Trying to resume stream for message that is currently streaming:' , messageId )
202216
203- // Currently we doesn't support SSE resume yet
217+ // Currently SSE resume not implemented yet
204218 // await streamToMessage({ message, resumeStreamId: streamSessionId })
205219
206- // Using custom convex polling instead
220+ // Using custom convex polling resume instead
207221 await pollToMessage ({ message , resumeStreamId: streamSessionId })
208222}
209223
@@ -218,22 +232,26 @@ async function pollToMessage({ message, resumeStreamId, threadId = threadIdRef.v
218232 return
219233 }
220234
235+ streamingMessagesMap [resumeStreamId ] = true
236+ console .log (` Polling: ${message .id } ` )
237+
221238 const messageFromConvex = await convex .query (api .messages .get , {
222239 messageId: message ._id ,
223240 lockerKey: getLockerKey (threadId ),
224241 })
225- Object .assign (message , customMessageTransform (messageFromConvex ))
242+ Object .assign (message , objectPick (messageFromConvex , [ ' content ' , ' context ' , ' isStreaming ' ] ))
226243
227244 if (message .isStreaming ) {
228245 // Wraps in a kontroller to make sure there is only one stream on the same message
229246 countdown (
230247 500 ,
231- () => pollToMessage ({ message , resumeStreamId , threadId }),
232- { key: ` messageStream-${message . id } ` },
248+ () => { nextTick (() => { pollToMessage ({ message , resumeStreamId , threadId }) }) } ,
249+ { key: ` messageStream-${resumeStreamId } ` },
233250 )
234251 }
235252 else {
236- console .log (' Poll completed' )
253+ console .log (` Poll completed: ${message .id } ` )
254+ delete streamingMessagesMap [resumeStreamId ]
237255 }
238256
239257 nextTick (() => { doScrollBottom ({ maybe: true }) })
@@ -242,17 +260,19 @@ async function pollToMessage({ message, resumeStreamId, threadId = threadIdRef.v
242260interface StreamToMessageArgs {
243261 message: CustomMessage
244262 content? : string
263+ streamId? : string
245264 resumeStreamId? : string
246265}
247- async function streamToMessage({ message , content , resumeStreamId }: StreamToMessageArgs ) {
266+ async function streamToMessage({ message , content , streamId , resumeStreamId }: StreamToMessageArgs ) {
248267 try {
249- ++ streamingMessages . value
268+ streamingMessagesMap [( streamId ?? resumeStreamId ) ! ] = true
250269
251270 const currentThreadId = threadIdRef .value
252271 const { response, abortController } = await postChatStream ({
253272 threadId: currentThreadId as Id <' threads' >,
254273 ... chatContext .activeAgent .value ,
255274 content ,
275+ streamId ,
256276 resumeStreamId ,
257277 })
258278
@@ -315,6 +335,8 @@ async function streamToMessage({ message, content, resumeStreamId }: StreamToMes
315335 if (state .content )
316336 message .content += state .content
317337
338+ console .log ({ chunk , write: state .content })
339+
318340 nextTick (() => { doScrollBottom ({ maybe: true }) })
319341 }
320342
@@ -325,14 +347,14 @@ async function streamToMessage({ message, content, resumeStreamId }: StreamToMes
325347 message ! .content += ` \n Error: ${(error as Error ).message }`
326348 }
327349 finally {
328- -- streamingMessages . value
350+ delete streamingMessagesMap [( streamId ?? resumeStreamId ) ! ]
329351 }
330352
331353 console .log (' Stream completed' )
332354}
333355
334356async function _branchThreadFromMessage({ messageId , lockerKey }: BranchThreadFromMessageArgs ) {
335- if (streamingMessages . value > 0 )
357+ if (Object . keys ( streamingMessagesMap ). length > 0 )
336358 throw new Error (' Can not branch while streaming' )
337359
338360 const messagesLte = messages .value .slice (0 , messages .value .findIndex (m => m ._id === messageId ) + 1 )
@@ -417,7 +439,7 @@ function doScrollBottom({ smooth = true, maybe = false, tries = 0, lastScrollTop
417439 </VueLenis >
418440
419441 <PrompterArea
420- v-bind =" { nearTopBottom, lenisRef, streamingMessages }"
442+ v-bind =" { nearTopBottom, lenisRef, streamingMessagesMap }"
421443 v-model:chat-input =" chatInput"
422444 @submit =" (input) => handleSubmit({ input })"
423445 />
0 commit comments