Skip to content

Commit 47a4ea4

Browse files
authored
Feature/add realtime updates (#333)
* Add realtime updates * Show realtime updates in planning messages * Implement streaming text renderer * Implement multi-comment streaming * Use ChatContent component to render streams
1 parent c01a5a2 commit 47a4ea4

File tree

3 files changed

+100
-23
lines changed

3 files changed

+100
-23
lines changed

web/src/components/common/ActionStack.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ const PlanningActionStack: React.FC = () => {
313313
const metadataProcessingCache = useSelector((state: RootState) => state.settings.metadataProcessingCache)
314314
const isAnalystmode = useSelector((state: RootState) => state.settings.analystMode) || false;
315315
const dbMetadata = get(metadataProcessingCache, [dbId, 'result'], null);
316-
316+
const thread = useSelector((state: RootState) => state.chat.activeThread)
317+
const planningMessage = useSelector((state: RootState) => state.chat.threads[thread].planningMessage)
318+
const streamingContents = useSelector((state: RootState) => state.chat.threads[thread].streamingContents)
319+
317320
const planningActions = isEmpty(dbMetadata) && isAnalystmode
318321
? ['One time optimizations underway']
319322
: ['Planning next steps', 'Thinking about the question', 'Understanding App state', 'Finalizing Actions', 'Validating Answers']
@@ -325,8 +328,11 @@ const PlanningActionStack: React.FC = () => {
325328
return () => clearInterval(intervalId);
326329
}, []);
327330

331+
// Use socket planning message if available, otherwise cycle through default messages
332+
const displayMessage = planningMessage || planningActions[currentTitleIndex % planningActions.length]
333+
328334
return (
329-
<HStack aria-label={"planning"} className={'action-stack'} justifyContent={'start'} width={"100%"}>
335+
<VStack aria-label={"planning"} className={'action-stack'} justifyContent={'start'} width={"100%"} spacing={2}>
330336
<Box
331337
bg={'minusxGreen.800'}
332338
p={2}
@@ -339,10 +345,31 @@ const PlanningActionStack: React.FC = () => {
339345
>
340346
<HStack>
341347
<Box>
342-
<Text key={currentTitleIndex} animation={currentTitleIndex > 0 ? `${scrollUp} 0.5s ease-in-out` : ""} >{planningActions[currentTitleIndex % planningActions.length]}</Text>
348+
<Text key={planningMessage || currentTitleIndex} animation={currentTitleIndex > 0 ? `${scrollUp} 0.5s ease-in-out` : ""} >{displayMessage}</Text>
343349
</Box>
344350
<Spinner size="xs" speed={'0.75s'} color="minusxBW.100" aria-label={"planning-spinner"}/>
345351
</HStack>
346352
</Box>
347-
</HStack>
353+
{streamingContents?.map((streamingContent) => (
354+
<Box
355+
key={streamingContent.id}
356+
bg={'minusxGreen.800'}
357+
p={3}
358+
borderRadius={'10px 10px 10px 0'}
359+
color={'minusxBW.50'}
360+
width={"90%"}
361+
alignSelf={'flex-start'}
362+
aria-label={"streaming-content-bubble"}
363+
>
364+
<ChatContent
365+
content={{
366+
type: 'DEFAULT',
367+
text: streamingContent.text,
368+
images: [],
369+
}}
370+
role="assistant"
371+
/>
372+
</Box>
373+
))}
374+
</VStack>
348375
)}

web/src/components/common/App.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { SupportButton } from './Support'
4646
import { Markdown } from './Markdown'
4747
import { getMXToken, setMinusxMode, toggleMinusXRoot } from '../../app/rpc'
4848
import { configs } from '../../constants'
49-
import { abortPlan, startNewThread, updateThreadID } from '../../state/chat/reducer'
49+
import { abortPlan, startNewThread, updateThreadID, setPlanningMessage, appendStreamingContent } from '../../state/chat/reducer'
5050
import { intelligentThreadStart } from '../../helpers/threadHistory'
5151

5252
// Agent constants
@@ -192,22 +192,31 @@ const AppLoggedIn = forwardRef((_props, ref) => {
192192
}, [atlasData, atlasLoading, atlasError])
193193

194194
// Disabling sockets for now
195-
// useSocketIO({
196-
// sessionToken: sessionJwt,
197-
// onMessage: (message) => {
198-
// console.log('Socket.io message received:', message);
199-
// },
200-
// onConnect: () => {
201-
// console.log('Socket.io connected successfully');
202-
// },
203-
// onDisconnect: (reason) => {
204-
// console.log('Socket.io disconnected:', reason);
205-
// },
206-
// onError: (error) => {
207-
// console.log(error.message)
208-
// console.error('Socket.io connection error:', error);
209-
// }
210-
// });
195+
useSocketIO({
196+
sessionToken: sessionJwt,
197+
onMessage: (message) => {
198+
console.log('Socket.io message received:', message);
199+
// Handle planning messages
200+
if (message?.type === 'message' && message?.content?.agent) {
201+
const agentName = message.content.agent;
202+
dispatch(setPlanningMessage(`Running ${agentName}`));
203+
}
204+
// Handle streaming content chunks
205+
if (message?.type === 'content' && message?.id && message?.content) {
206+
dispatch(appendStreamingContent({ id: message.id, chunk: message.content }));
207+
}
208+
},
209+
onConnect: () => {
210+
console.log('Socket.io connected successfully');
211+
},
212+
onDisconnect: (reason) => {
213+
console.log('Socket.io disconnected:', reason);
214+
},
215+
onError: (error) => {
216+
console.log(error.message)
217+
console.error('Socket.io connection error:', error);
218+
}
219+
});
211220

212221
// Update thread id on start
213222
useEffect(() => {

web/src/state/chat/reducer.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ export interface ChatThread {
153153
interrupted: boolean
154154
tasks: Tasks
155155
id: string
156+
planningMessage?: string
157+
streamingContents?: Array<{ id: string, text: string }>
156158
}
157159

158160
interface ChatState {
@@ -735,7 +737,14 @@ export const chatSlice = createSlice({
735737
activeThread.debugChatIndex = action.payload
736738
},
737739
setActiveThreadStatus: (state, action: PayloadAction<ChatThreadStatus>) => {
738-
state.threads[state.activeThread].status = action.payload
740+
const activeThread = state.threads[state.activeThread]
741+
const oldStatus = activeThread.status
742+
activeThread.status = action.payload
743+
// Clear planning data when leaving PLANNING status
744+
if (oldStatus === 'PLANNING' && action.payload !== 'PLANNING') {
745+
activeThread.planningMessage = undefined
746+
activeThread.streamingContents = undefined
747+
}
739748
},
740749
toggleUserConfirmation: (state, action: PayloadAction<{
741750
show: boolean
@@ -884,11 +893,43 @@ export const chatSlice = createSlice({
884893
console.error('Error cloning thread from history:', error)
885894
// Don't change state on error - let existing thread remain active
886895
}
896+
},
897+
setPlanningMessage: (state, action: PayloadAction<string>) => {
898+
const activeThread = getActiveThread(state)
899+
activeThread.planningMessage = action.payload
900+
},
901+
clearPlanningMessage: (state) => {
902+
const activeThread = getActiveThread(state)
903+
activeThread.planningMessage = undefined
904+
},
905+
appendStreamingContent: (state, action: PayloadAction<{ id: string, chunk: string }>) => {
906+
const activeThread = getActiveThread(state)
907+
const { id, chunk } = action.payload
908+
909+
// Initialize array if needed
910+
if (!activeThread.streamingContents) {
911+
activeThread.streamingContents = []
912+
}
913+
914+
// Find existing entry by id
915+
const existingEntry = activeThread.streamingContents.find(entry => entry.id === id)
916+
917+
if (existingEntry) {
918+
// Append chunk to existing entry
919+
existingEntry.text += chunk
920+
} else {
921+
// Add new entry
922+
activeThread.streamingContents.push({ id, text: chunk })
923+
}
924+
},
925+
clearStreamingContent: (state) => {
926+
const activeThread = getActiveThread(state)
927+
activeThread.streamingContents = undefined
887928
}
888929
},
889930
})
890931

891932
// Action creators are generated for each case reducer function
892-
export const { addUserMessage, deleteUserMessage, addActionPlanMessage, addActionPlanMessageV2, startAction, finishAction, interruptPlan, startNewThread, addReaction, removeReaction, updateDebugChatIndex, setActiveThreadStatus, toggleUserConfirmation, setUserConfirmationInput, toggleClarification, setClarificationAnswer, switchToThread, abortPlan, updateThreadID, updateLastWarmedOn, clearTasks, cloneThreadFromHistory, setUserConfirmationFeedback } = chatSlice.actions
933+
export const { addUserMessage, deleteUserMessage, addActionPlanMessage, addActionPlanMessageV2, startAction, finishAction, interruptPlan, startNewThread, addReaction, removeReaction, updateDebugChatIndex, setActiveThreadStatus, toggleUserConfirmation, setUserConfirmationInput, toggleClarification, setClarificationAnswer, switchToThread, abortPlan, updateThreadID, updateLastWarmedOn, clearTasks, cloneThreadFromHistory, setUserConfirmationFeedback, setPlanningMessage, clearPlanningMessage, appendStreamingContent, clearStreamingContent } = chatSlice.actions
893934

894935
export default chatSlice.reducer

0 commit comments

Comments
 (0)