diff --git a/src/backend/common/models/messages_kernel.py b/src/backend/common/models/messages_kernel.py index f0a22646..e1951615 100644 --- a/src/backend/common/models/messages_kernel.py +++ b/src/backend/common/models/messages_kernel.py @@ -125,6 +125,7 @@ class Plan(BaseDataModel): m_plan: Optional[Dict[str, Any]] = None summary: Optional[str] = None team_id: Optional[str] = None + streaming_message: Optional[str] = None human_clarification_request: Optional[str] = None human_clarification_response: Optional[str] = None diff --git a/src/backend/v3/api/router.py b/src/backend/v3/api/router.py index 746e9c27..d133715a 100644 --- a/src/backend/v3/api/router.py +++ b/src/backend/v3/api/router.py @@ -1337,12 +1337,15 @@ async def get_plan_by_id(request: Request, plan_id: Optional[str] = Query(None) team = await memory_store.get_team_by_id(team_id=plan.team_id) agent_messages = await memory_store.get_agent_messages(plan_id=plan.plan_id) mplan = plan.m_plan if plan.m_plan else None + streaming_message = plan.streaming_message if plan.streaming_message else "" + plan.streaming_message = "" # clear streaming message after retrieval plan.m_plan = None # remove m_plan from plan object for response return { "plan": plan, "team": team if team else None, "messages": agent_messages, "m_plan": mplan, + "streaming_message": streaming_message, } else: track_event_if_configured( diff --git a/src/backend/v3/common/services/plan_service.py b/src/backend/v3/common/services/plan_service.py index 46f6a450..1c0da50b 100644 --- a/src/backend/v3/common/services/plan_service.py +++ b/src/backend/v3/common/services/plan_service.py @@ -215,6 +215,7 @@ async def handle_agent_messages( await memory_store.add_agent_message(agent_msg) if agent_message.is_final: plan = await memory_store.get_plan(agent_msg.plan_id) + plan.streaming_message = agent_message.streaming_message plan.overall_status = PlanStatus.completed await memory_store.update_plan(plan) return True diff --git a/src/backend/v3/models/messages.py b/src/backend/v3/models/messages.py index 25d42f04..4605723f 100644 --- a/src/backend/v3/models/messages.py +++ b/src/backend/v3/models/messages.py @@ -146,6 +146,7 @@ class AgentMessageResponse: agent_type: AgentMessageType is_final: bool = False raw_data: str = None + streaming_message: str = None class WebsocketMessageType(str, Enum): diff --git a/src/frontend/src/api/apiService.tsx b/src/frontend/src/api/apiService.tsx index e28f1184..59280054 100644 --- a/src/frontend/src/api/apiService.tsx +++ b/src/frontend/src/api/apiService.tsx @@ -166,7 +166,8 @@ export class APIService { plan: data.plan as Plan, messages: data.messages as AgentMessageBE[], m_plan: data.m_plan as MPlanBE | null, - team: data.team as TeamConfigurationBE | null + team: data.team as TeamConfigurationBE | null, + streaming_message: data.streaming_message as string | null } as PlanFromAPI; if (useCache) { this._cache.set(cacheKey, results, 30000); // Cache for 30 seconds diff --git a/src/frontend/src/components/content/PlanChat.tsx b/src/frontend/src/components/content/PlanChat.tsx index 8bd0bed0..57a1c805 100644 --- a/src/frontend/src/components/content/PlanChat.tsx +++ b/src/frontend/src/components/content/PlanChat.tsx @@ -31,8 +31,8 @@ import renderPlanResponse from "./streaming/StreamingPlanResponse"; import { renderPlanExecutionMessage, renderThinkingState } from "./streaming/StreamingPlanState"; import ContentNotFound from "../NotFound/ContentNotFound"; import PlanChatBody from "./PlanChatBody"; -import renderBufferMessage from "./streaming/StreamingBufferMessage"; import renderAgentMessages from "./streaming/StreamingAgentMessage"; +import StreamingBufferMessage from "./streaming/StreamingBufferMessage"; interface SimplifiedPlanChatProps extends PlanChatProps { onPlanReceived?: (planData: MPlanData) => void; @@ -41,6 +41,7 @@ interface SimplifiedPlanChatProps extends PlanChatProps { waitingForPlan: boolean; messagesContainerRef: React.RefObject; streamingMessageBuffer: string; + showBufferingText: boolean; agentMessages: AgentMessageData[]; showProcessingPlanSpinner: boolean; showApprovalButtons: boolean; @@ -63,26 +64,16 @@ const PlanChat: React.FC = ({ waitingForPlan, messagesContainerRef, streamingMessageBuffer, + showBufferingText, agentMessages, showProcessingPlanSpinner, showApprovalButtons, handleApprovePlan, handleRejectPlan, processingApproval - - }) => { - const navigate = useNavigate(); - - const { showToast, dismissToast } = useInlineToaster(); // States - - - - - - if (!planData) return ( @@ -119,8 +110,12 @@ const PlanChat: React.FC = ({ {showProcessingPlanSpinner && renderPlanExecutionMessage()} {/* Streaming plan updates */} - {renderBufferMessage(streamingMessageBuffer)} - + {showBufferingText && ( + + )} {/* Chat Input - only show if no plan is waiting for approval */} @@ -132,7 +127,7 @@ const PlanChat: React.FC = ({ OnChatSubmit={OnChatSubmit} waitingForPlan={waitingForPlan} loading={false} /> - + ); }; diff --git a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx index 4763dc0e..538f2a16 100644 --- a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx @@ -12,7 +12,11 @@ interface StreamingBufferMessageProps { isStreaming?: boolean; } -const renderBufferMessage = (streamingMessageBuffer: string, isStreaming: boolean = false) => { +// Convert to a proper React component instead of a function +const StreamingBufferMessage: React.FC = ({ + streamingMessageBuffer, + isStreaming = false +}) => { const [isExpanded, setIsExpanded] = useState(false); const [shouldFade, setShouldFade] = useState(false); const contentRef = useRef(null); @@ -51,11 +55,11 @@ const renderBufferMessage = (streamingMessageBuffer: string, isStreaming: boolea padding: '16px', fontSize: '14px', lineHeight: '1.5', - height: isExpanded ? 'auto' : '256px', // Auto height when expanded + height: isExpanded ? 'auto' : '256px', display: 'flex', flexDirection: 'column', position: 'relative', - overflow: isExpanded ? 'visible' : 'hidden' // Allow overflow when expanded + overflow: isExpanded ? 'visible' : 'hidden' }}> {/* Header */}
)} - {/* Content area - expanded state (original behavior) */} + {/* Content area - expanded state */} {isExpanded && (
{ const [wsConnected, setWsConnected] = useState(false); const [streamingMessages, setStreamingMessages] = useState([]); const [streamingMessageBuffer, setStreamingMessageBuffer] = useState(""); - + const [showBufferingText, setShowBufferingText] = useState(false); const [agentMessages, setAgentMessages] = useState([]); // Plan approval state - track when plan is approved @@ -62,10 +62,10 @@ const PlanPage: React.FC = () => { - const processAgentMessage = useCallback((agentMessageData: AgentMessageData, planData: ProcessedPlanData, is_final: boolean = false) => { + const processAgentMessage = useCallback((agentMessageData: AgentMessageData, planData: ProcessedPlanData, is_final: boolean = false, streaming_message: string = '') => { // Persist / forward to backend (fire-and-forget with logging) - const agentMessageResponse = PlanDataService.createAgentMessageResponse(agentMessageData, planData, is_final); + const agentMessageResponse = PlanDataService.createAgentMessageResponse(agentMessageData, planData, is_final, streaming_message); console.log('📤 Persisting agent message:', agentMessageResponse); void apiService.sendAgentMessage(agentMessageResponse) .then(saved => { @@ -82,9 +82,44 @@ const PlanPage: React.FC = () => { }, []); const resetPlanVariables = useCallback(() => { - - - }, []); + setInput(""); + setPlanData(null); + setLoading(true); + setSubmittingChatDisableInput(true); + setErrorLoading(false); + setClarificationMessage(null); + setProcessingApproval(false); + setPlanApprovalRequest(null); + setReloadLeftList(true); + setWaitingForPlan(true); + setShowProcessingPlanSpinner(false); + setShowApprovalButtons(true); + setContinueWithWebsocketFlow(false); + setWsConnected(false); + setStreamingMessages([]); + setStreamingMessageBuffer(""); + setShowBufferingText(false); + setAgentMessages([]); + }, [ + setInput, + setPlanData, + setLoading, + setSubmittingChatDisableInput, + setErrorLoading, + setClarificationMessage, + setProcessingApproval, + setPlanApprovalRequest, + setReloadLeftList, + setWaitingForPlan, + setShowProcessingPlanSpinner, + setShowApprovalButtons, + setContinueWithWebsocketFlow, + setWsConnected, + setStreamingMessages, + setStreamingMessageBuffer, + setShowBufferingText, + setAgentMessages + ]); // Auto-scroll helper const scrollToBottom = useCallback(() => { @@ -146,6 +181,7 @@ const PlanPage: React.FC = () => { //console.log('📋 Streaming Message', streamingMessage); // if is final true clear buffer and add final message to agent messages const line = PlanDataService.simplifyHumanClarification(streamingMessage.data.content); + setShowBufferingText(true); setStreamingMessageBuffer(prev => prev + line); //scrollToBottom(); @@ -175,7 +211,7 @@ const PlanPage: React.FC = () => { console.log('✅ Parsed clarification message:', agentMessageData); setClarificationMessage(clarificationMessage.data as ParsedUserClarification | null); setAgentMessages(prev => [...prev, agentMessageData]); - setStreamingMessageBuffer(""); + setShowBufferingText(false); setShowProcessingPlanSpinner(false); setSubmittingChatDisableInput(false); scrollToBottom(); @@ -221,7 +257,8 @@ const PlanPage: React.FC = () => { console.log('✅ Parsed final result message:', agentMessageData); // we ignore the terminated message if (finalMessage?.data?.status === PlanStatus.COMPLETED) { - setStreamingMessageBuffer(""); + + setShowBufferingText(true); setShowProcessingPlanSpinner(false); setAgentMessages(prev => [...prev, agentMessageData]); scrollToBottom(); @@ -232,14 +269,14 @@ const PlanPage: React.FC = () => { setPlanData({ ...planData }); } - processAgentMessage(agentMessageData, planData, is_final); + processAgentMessage(agentMessageData, planData, is_final, streamingMessageBuffer); } }); return () => unsubscribe(); - }, [scrollToBottom, planData, processAgentMessage]); + }, [scrollToBottom, planData, processAgentMessage, streamingMessageBuffer]); //WebsocketMessageType.AGENT_MESSAGE useEffect(() => { @@ -340,7 +377,7 @@ const PlanPage: React.FC = () => { const loadPlanData = useCallback( async (useCache = true): Promise => { if (!planId) return null; - + resetPlanVariables(); setLoading(true); try { @@ -364,6 +401,10 @@ const PlanPage: React.FC = () => { if (planResult?.mplan) { setPlanApprovalRequest(planResult.mplan); } + if (planResult?.streaming_message && planResult.streaming_message.trim() !== "") { + setStreamingMessageBuffer(planResult.streaming_message); + setShowBufferingText(true); + } setPlanData(planResult); return planResult; } catch (err) { @@ -373,7 +414,7 @@ const PlanPage: React.FC = () => { setLoading(false); } }, - [planId, navigate] + [planId, navigate, resetPlanVariables] ); @@ -595,6 +636,7 @@ const PlanPage: React.FC = () => { waitingForPlan={waitingForPlan} messagesContainerRef={messagesContainerRef} streamingMessageBuffer={streamingMessageBuffer} + showBufferingText={showBufferingText} agentMessages={agentMessages} showProcessingPlanSpinner={showProcessingPlanSpinner} showApprovalButtons={showApprovalButtons} diff --git a/src/frontend/src/services/PlanDataService.tsx b/src/frontend/src/services/PlanDataService.tsx index dc4e9ba9..70cd64e6 100644 --- a/src/frontend/src/services/PlanDataService.tsx +++ b/src/frontend/src/services/PlanDataService.tsx @@ -41,6 +41,7 @@ export class PlanDataService { try { // Use optimized getPlanById method for better performance const planBody = await apiService.getPlanById(planId, useCache); + console.log('Raw plan data fetched:', planBody); return this.processPlanData(planBody); } catch (error) { console.log("Failed to fetch plan data:", error); @@ -209,11 +210,13 @@ export class PlanDataService { const team = this.convertTeamConfiguration(planFromAPI.team); const mplan = this.convertMPlan(planFromAPI.m_plan); const messages: AgentMessageData[] = this.convertAgentMessages(planFromAPI.messages || []); + const streaming_message = planFromAPI.streaming_message || null; return { plan, team, mplan, - messages + messages, + streaming_message }; } @@ -226,20 +229,20 @@ export class PlanDataService { static createAgentMessageResponse( agentMessage: AgentMessageData, planData: ProcessedPlanData, - is_final: boolean = false + is_final: boolean = false, + streaming_message: string = '' ): AgentMessageResponse { if (!planData || !planData.plan) { console.log("Invalid plan data provided to createAgentMessageResponse"); } return { - plan_id: planData.plan.plan_id, agent: agentMessage.agent, content: agentMessage.content, agent_type: agentMessage.agent_type, is_final: is_final, raw_data: JSON.stringify(agentMessage.raw_data), - + streaming_message: streaming_message }; }