Skip to content

Commit 11f0c66

Browse files
committed
Add streaming buffer message display to PlanChat
Introduces a new StreamingBufferMessage component to render live agent message streams in PlanChat. Updates PlanPage and WebSocketService to handle and buffer streaming agent messages, improving real-time feedback during plan generation.
1 parent 44d9467 commit 11f0c66

File tree

5 files changed

+81
-49
lines changed

5 files changed

+81
-49
lines changed

src/frontend/src/components/content/PlanChat.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ import renderPlanResponse from "./streaming/StreamingPlanResponse";
3131
import renderThinkingState from "./streaming/StreamingPlanState";
3232
import ContentNotFound from "../NotFound/ContentNotFound";
3333
import PlanChatBody from "./PlanChatBody";
34+
import renderBufferMessage from "./streaming/StreamingBufferMessage";
3435
interface SimplifiedPlanChatProps extends PlanChatProps {
3536
onPlanReceived?: (planData: MPlanData) => void;
3637
initialTask?: string;
3738
planApprovalRequest: MPlanData | null;
3839
waitingForPlan: boolean;
3940
messagesContainerRef: React.RefObject<HTMLDivElement>;
41+
streamingMessageBuffer: string;
42+
agentMessages: any[];
4043
}
4144

4245
const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
@@ -50,7 +53,9 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
5053
initialTask,
5154
planApprovalRequest,
5255
waitingForPlan,
53-
messagesContainerRef
56+
messagesContainerRef,
57+
streamingMessageBuffer,
58+
agentMessages
5459

5560
}) => {
5661
const navigate = useNavigate();
@@ -154,6 +159,9 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
154159

155160
{/* Plan response with all information */}
156161
{renderPlanResponse(planApprovalRequest, handleApprovePlan, handleRejectPlan, processingApproval, showApprovalButtons)}
162+
163+
{/* Streaming plan updates */}
164+
{renderBufferMessage(streamingMessageBuffer)}
157165
</div>
158166

159167
{/* Chat Input - only show if no plan is waiting for approval */}

src/frontend/src/components/content/PlanChatBody.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const PlanChatBody: React.FC<SimplifiedPlanChatProps> = ({
1515
OnChatSubmit,
1616
showChatInput,
1717
waitingForPlan
18-
1918
}) => {
2019
if (!showChatInput) {
2120
return null;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {
2+
Accordion,
3+
AccordionItem,
4+
AccordionHeader,
5+
AccordionPanel,
6+
} from '@fluentui/react-components';
7+
import ReactMarkdown from "react-markdown";
8+
import remarkGfm from "remark-gfm";
9+
import rehypePrism from "rehype-prism";
10+
import { useState } from "react";
11+
// Render AI thinking/planning state
12+
const renderBufferMessage = (streamingMessageBuffer: string) => {
13+
if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null;
14+
return (
15+
<Accordion collapsible defaultOpenItems="one">
16+
<AccordionItem value="one">
17+
18+
<AccordionPanel>
19+
<ReactMarkdown
20+
remarkPlugins={[remarkGfm]}
21+
rehypePlugins={[rehypePrism]}
22+
>
23+
{streamingMessageBuffer}
24+
</ReactMarkdown>
25+
</AccordionPanel>
26+
</AccordionItem>
27+
</Accordion>
28+
29+
);
30+
};
31+
32+
export default renderBufferMessage;

src/frontend/src/pages/PlanPage.tsx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ const PlanPage: React.FC = () => {
5353
// WebSocket connection state
5454
const [wsConnected, setWsConnected] = useState(false);
5555
const [streamingMessages, setStreamingMessages] = useState<StreamingPlanUpdate[]>([]);
56-
56+
const [streamingMessageBuffer, setStreamingMessageBuffer] = useState<string>("");
5757
// RAI Error state
5858
const [raiError, setRAIError] = useState<RAIErrorData | null>(null);
5959

60+
const [agentMessages, setAgentMessages] = useState<any[]>([]);
6061
// Team config state
6162
const [teamConfig, setTeamConfig] = useState<TeamConfig | null>(null);
6263
const [loadingTeamConfig, setLoadingTeamConfig] = useState(true);
@@ -72,7 +73,11 @@ const PlanPage: React.FC = () => {
7273
const scrollToBottom = useCallback(() => {
7374
setTimeout(() => {
7475
if (messagesContainerRef.current) {
75-
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
76+
//messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
77+
messagesContainerRef.current?.scrollTo({
78+
top: messagesContainerRef.current.scrollHeight,
79+
behavior: "smooth",
80+
});
7681
}
7782
}, 100);
7883
}, []);
@@ -116,6 +121,29 @@ const PlanPage: React.FC = () => {
116121

117122
return () => unsubscribe();
118123
}, [scrollToBottom]); //onPlanReceived, scrollToBottom
124+
125+
126+
useEffect(() => {
127+
const unsubscribe = webSocketService.on(WebsocketMessageType.AGENT_MESSAGE_STREAMING, (streamingMessage: any) => {
128+
// console.log('📋 Streaming Message', streamingMessage);
129+
setStreamingMessageBuffer(prev => prev + streamingMessage.data.content);
130+
scrollToBottom();
131+
132+
});
133+
134+
return () => unsubscribe();
135+
}, [scrollToBottom]); //onPlanReceived, scrollToBottom
136+
137+
useEffect(() => {
138+
const unsubscribe = webSocketService.on(WebsocketMessageType.AGENT_MESSAGE, (agentMessage: any) => {
139+
console.log('📋 Agent Message', agentMessage);
140+
setAgentMessages(prev => [...prev, agentMessage]);
141+
scrollToBottom();
142+
});
143+
144+
return () => unsubscribe();
145+
}, [scrollToBottom]); //onPlanReceived, scrollToBottom
146+
119147
// Loading message rotation effect
120148
useEffect(() => {
121149
let interval: NodeJS.Timeout;
@@ -429,19 +457,6 @@ const PlanPage: React.FC = () => {
429457
</PanelRightToggles>
430458
</ContentToolbar>
431459

432-
{/* Show RAI error if present */}
433-
{raiError && (
434-
<div style={{ padding: '16px 24px 0' }}>
435-
<RAIErrorCard
436-
error={raiError}
437-
onRetry={() => {
438-
setRAIError(null);
439-
}}
440-
onDismiss={() => setRAIError(null)}
441-
/>
442-
</div>
443-
)}
444-
445460
<PlanChat
446461
planData={planData}
447462
OnChatSubmit={handleOnchatSubmit}
@@ -455,6 +470,9 @@ const PlanPage: React.FC = () => {
455470
planApprovalRequest={planApprovalRequest}
456471
waitingForPlan={waitingForPlan}
457472
messagesContainerRef={messagesContainerRef}
473+
streamingMessageBuffer={streamingMessageBuffer}
474+
agentMessages={agentMessages}
475+
458476
/>
459477
</>
460478
)}

src/frontend/src/services/WebSocketService.tsx

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,14 @@ class WebSocketService {
165165
}
166166

167167
private handleMessage(message: StreamMessage): void {
168-
console.log('WebSocket message received:', message);
168+
169169
const currentPlanIds = Array.from(this.planSubscriptions);
170170
const firstPlanId = currentPlanIds[0];
171171

172172
switch (message.type) {
173173
case WebsocketMessageType.PLAN_APPROVAL_REQUEST: {
174174
console.log("enter plan approval request");
175+
console.log('WebSocket message received:', message);
175176
const parsedData = PlanDataService.parsePlanApprovalRequest(message.data);
176177
if (parsedData) {
177178
const structuredMessage: ParsedPlanApprovalRequest = {
@@ -189,44 +190,18 @@ class WebSocketService {
189190

190191
case WebsocketMessageType.AGENT_MESSAGE: {
191192
if (message.data) {
192-
const transformed: StreamMessage = {
193-
...message,
194-
data: {
195-
plan_id: firstPlanId,
196-
agent_name: message.data.agent_name || 'Unknown Agent',
197-
content: message.data.content || '',
198-
message_type: 'thinking',
199-
status: 'in_progress',
200-
timestamp: Date.now() / 1000
201-
}
202-
};
193+
console.log('WebSocket message received:', message);
194+
const transformed = PlanDataService.parseAgentMessage(message);
203195
this.emit(WebsocketMessageType.AGENT_MESSAGE, transformed);
204-
} else {
205-
this.emit(WebsocketMessageType.AGENT_MESSAGE, message);
196+
206197
}
207198
break;
208199
}
209200

210201
case WebsocketMessageType.AGENT_MESSAGE_STREAMING: {
211202
if (message.data) {
212-
const isFinal = !!message.data.is_final;
213-
const transformed: StreamMessage = {
214-
type: WebsocketMessageType.AGENT_MESSAGE,
215-
data: {
216-
plan_id: message.data.plan_id || firstPlanId,
217-
agent_name: message.data.agent_name || 'Unknown Agent',
218-
content: message.data.content || '',
219-
message_type: isFinal ? 'result' : 'thinking',
220-
status: isFinal ? 'completed' : 'in_progress',
221-
timestamp: Date.now() / 1000,
222-
is_final: isFinal
223-
}
224-
};
225-
if (!transformed.data.plan_id) {
226-
console.warn('Streaming message missing plan_id and no subscription context.');
227-
break;
228-
}
229-
this.emit(WebsocketMessageType.AGENT_MESSAGE, transformed);
203+
const streamedMessage = PlanDataService.parseAgentMessageStreaming(message);
204+
this.emit(WebsocketMessageType.AGENT_MESSAGE_STREAMING, streamedMessage);
230205
}
231206
break;
232207
}

0 commit comments

Comments
 (0)