From f6713c5d8210bfb6958d523cdf1e34377b744872 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 23 Jun 2026 14:42:40 +0530 Subject: [PATCH 1/2] commit --- .../common/ProcessingStatusIndicator.tsx | 60 +++++++++++++++++++ src/App/src/components/content/PlanChat.tsx | 8 ++- .../content/streaming/StreamingPlanState.tsx | 34 +++-------- src/App/src/hooks/usePlanWebSocket.tsx | 34 ++++++++++- src/App/src/pages/PlanPage.tsx | 43 +++++++++++++ 5 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 src/App/src/components/common/ProcessingStatusIndicator.tsx diff --git a/src/App/src/components/common/ProcessingStatusIndicator.tsx b/src/App/src/components/common/ProcessingStatusIndicator.tsx new file mode 100644 index 000000000..c53a90e19 --- /dev/null +++ b/src/App/src/components/common/ProcessingStatusIndicator.tsx @@ -0,0 +1,60 @@ +import { Spinner } from '@fluentui/react-components'; + +interface ProcessingStatusIndicatorProps { + message: string; + elapsedSeconds?: number; +} + +const formatElapsedTime = (elapsedSeconds: number): string => { + if (elapsedSeconds < 60) { + return `${elapsedSeconds}s`; + } + + const minutes = Math.floor(elapsedSeconds / 60); + const seconds = elapsedSeconds % 60; + return `${minutes}min ${seconds}sec`; +}; + +const ProcessingStatusIndicator = ({ + message, + elapsedSeconds, +}: ProcessingStatusIndicatorProps) => { + const showElapsedSuffix = Number.isFinite(elapsedSeconds) && (elapsedSeconds as number) > 0; + const elapsedSuffix = showElapsedSuffix ? ` (${formatElapsedTime(elapsedSeconds as number)})` : ''; + + return ( +
+
+ + + {message} + {elapsedSuffix} + +
+
+ ); +}; + +export default ProcessingStatusIndicator; diff --git a/src/App/src/components/content/PlanChat.tsx b/src/App/src/components/content/PlanChat.tsx index f65461a33..7a2d78d25 100644 --- a/src/App/src/components/content/PlanChat.tsx +++ b/src/App/src/components/content/PlanChat.tsx @@ -21,6 +21,8 @@ interface SimplifiedPlanChatProps extends PlanChatProps { showBufferingText: boolean; agentMessages: AgentMessageData[]; showProcessingPlanSpinner: boolean; + processingElapsedSeconds: number; + processingStatusMessage: string; showApprovalButtons: boolean; handleApprovePlan: () => Promise; handleRejectPlan: () => Promise; @@ -45,13 +47,13 @@ const PlanChat: React.FC = ({ showBufferingText, agentMessages, showProcessingPlanSpinner, + processingElapsedSeconds, + processingStatusMessage, showApprovalButtons, handleApprovePlan, handleRejectPlan, processingApproval }) => { - // States - if (!planData) return ( @@ -86,7 +88,7 @@ const PlanChat: React.FC = ({ {renderPlanResponse(planApprovalRequest, handleApprovePlan, handleRejectPlan, processingApproval, showApprovalButtons)} {renderAgentMessages(agentMessages, undefined, undefined, finalResultRef)} - {showProcessingPlanSpinner && renderPlanExecutionMessage()} + {showProcessingPlanSpinner && renderPlanExecutionMessage(processingElapsedSeconds, processingStatusMessage)} {/* Streaming plan updates — hidden while an approval prompt is pending so the approval action is presented at the appropriate step instead of after the thinking process visibly completes. */} diff --git a/src/App/src/components/content/streaming/StreamingPlanState.tsx b/src/App/src/components/content/streaming/StreamingPlanState.tsx index f881131ed..cc4728a6c 100644 --- a/src/App/src/components/content/streaming/StreamingPlanState.tsx +++ b/src/App/src/components/content/streaming/StreamingPlanState.tsx @@ -1,4 +1,5 @@ import { Spinner } from "@fluentui/react-components"; +import ProcessingStatusIndicator from "../../common/ProcessingStatusIndicator.tsx"; // Simple thinking message to show while creating plan const renderThinkingState = (waitingForPlan: boolean) => { @@ -54,32 +55,15 @@ const renderThinkingState = (waitingForPlan: boolean) => { }; // Simple message to show while executing the plan -const renderPlanExecutionMessage = () => { +const renderPlanExecutionMessage = ( + processingElapsedSeconds?: number, + processingStatusMessage = 'Processing your plan and coordinating with AI agents...', +) => { return ( -
-
- - - Processing your plan and coordinating with AI agents... - -
-
+ ); }; diff --git a/src/App/src/hooks/usePlanWebSocket.tsx b/src/App/src/hooks/usePlanWebSocket.tsx index eee6ce5cf..a210d5619 100644 --- a/src/App/src/hooks/usePlanWebSocket.tsx +++ b/src/App/src/hooks/usePlanWebSocket.tsx @@ -15,6 +15,7 @@ import { selectPlanData, selectContinueWithWebsocketFlow, selectPlanApproved, + selectShowProcessingPlanSpinner, approvalRequestReceived, planCompletedFinal, planFailedFinal, @@ -44,11 +45,10 @@ import { ProcessedPlanData, } from '@/models'; import { APIService } from '@/api/apiService'; +import { ToastIntent } from '@/components/toast/InlineToaster'; const apiService = new APIService(); -import { ToastIntent } from '@/components/toast/InlineToaster'; - interface UsePlanWebSocketProps { planId: string | undefined; scrollToBottom: () => void; @@ -57,6 +57,16 @@ interface UsePlanWebSocketProps { showToast: (content: React.ReactNode, intent?: ToastIntent, options?: { dismissible?: boolean; timeoutMs?: number | null }) => number; } +const formatElapsedTime = (elapsedSeconds: number): string => { + if (elapsedSeconds < 60) { + return `${elapsedSeconds}s`; + } + + const minutes = Math.floor(elapsedSeconds / 60); + const seconds = elapsedSeconds % 60; + return `${minutes}min ${seconds}sec`; +}; + /** * Creates an AgentMessageResponse and persists it, then optionally reloads the task list. */ @@ -99,8 +109,16 @@ export function usePlanWebSocket({ const dispatch = useAppDispatch(); const planData = useAppSelector(selectPlanData); const planApproved = useAppSelector(selectPlanApproved); + const showProcessingPlanSpinner = useAppSelector(selectShowProcessingPlanSpinner); const continueWithWebsocketFlow = useAppSelector(selectContinueWithWebsocketFlow); const streamingMessageBuffer = useAppSelector(selectStreamingMessageBuffer); + const processingStartedAtRef = React.useRef(null); + + useEffect(() => { + if (showProcessingPlanSpinner && processingStartedAtRef.current === null) { + processingStartedAtRef.current = Date.now(); + } + }, [showProcessingPlanSpinner]); // ── PLAN_APPROVAL_REQUEST ───────────────────────────────────── useEffect(() => { @@ -162,6 +180,7 @@ export function usePlanWebSocket({ dispatch(addAgentMessage(agentMessageData)); dispatch(setShowBufferingText(false)); dispatch(setShowProcessingPlanSpinner(false)); + processingStartedAtRef.current = null; dispatch(setSubmittingChatDisableInput(false)); scrollToBottom(); persistAgentMessage(agentMessageData, planData, dispatch); @@ -182,6 +201,12 @@ export function usePlanWebSocket({ WebsocketMessageType.FINAL_RESULT_MESSAGE, (finalMessage: any) => { if (!finalMessage) return; + const completionElapsedSeconds = processingStartedAtRef.current + ? Math.max(Math.round((Date.now() - processingStartedAtRef.current) / 1000), 0) + : null; + const completionTimeLine = completionElapsedSeconds !== null + ? `\n\n**Total completion time: ${formatElapsedTime(completionElapsedSeconds)}**` + : ''; const messageStatus = finalMessage?.data?.status; if (messageStatus === PlanStatus.COMPLETED) { @@ -191,7 +216,7 @@ export function usePlanWebSocket({ timestamp: Date.now(), steps: [], next_steps: [], - content: finalMessage.data?.content || '', + content: (finalMessage.data?.content || '') + completionTimeLine, raw_data: finalMessage, }; dispatch(setShowBufferingText(true)); @@ -199,6 +224,7 @@ export function usePlanWebSocket({ dispatch(setSelectedTeam(planData?.team || null)); /* P0: single compound action replaces setShowProcessingPlanSpinner(false) + markPlanCompleted() */ dispatch(planCompletedFinal()); + processingStartedAtRef.current = null; scrollToFinalResult(); webSocketService.disconnect(); persistAgentMessage(agentMessageData, planData, dispatch, true, streamingMessageBuffer); @@ -257,6 +283,8 @@ export function usePlanWebSocket({ }; dispatch(addAgentMessage(errorAgent)); dispatch(planFailedFinal()); + dispatch(setShowProcessingPlanSpinner(false)); + processingStartedAtRef.current = null; dispatch(setShowBufferingText(false)); dispatch(setSubmittingChatDisableInput(true)); scrollToBottom(); diff --git a/src/App/src/pages/PlanPage.tsx b/src/App/src/pages/PlanPage.tsx index 773a0e5e6..f7e2a68c8 100644 --- a/src/App/src/pages/PlanPage.tsx +++ b/src/App/src/pages/PlanPage.tsx @@ -78,6 +78,30 @@ import '../styles/PlanPage.css'; // Singleton API service const apiService = new APIService(); +const getPlanProcessingStatusMessage = (elapsedSeconds: number): string => { + if (elapsedSeconds < 8) { + return 'Processing your plan and coordinating with AI agents...'; + } + + if (elapsedSeconds < 20) { + return 'Assigning tasks to specialized agents...'; + } + + if (elapsedSeconds < 35) { + return 'Agents are analyzing and researching...'; + } + + if (elapsedSeconds < 50) { + return 'Compiling results from agents...'; + } + + if (elapsedSeconds < 90) { + return 'Finalizing responses...'; + } + + return 'Still processing, please wait...'; +}; + /* ================================================================ * PlanPage — refactored to use Redux + extracted hooks * ================================================================ */ @@ -114,6 +138,8 @@ const PlanPage: React.FC = () => { /* ── Cancellation alert hook ────────────────────────────── */ const [pendingNavigation, setPendingNavigation] = React.useState<(() => void) | null>(null); + const [processingElapsedSeconds, setProcessingElapsedSeconds] = React.useState(0); + const processingStatusMessage = getPlanProcessingStatusMessage(processingElapsedSeconds); const { isPlanActive } = usePlanCancellationAlert({ planData, @@ -288,6 +314,21 @@ const PlanPage: React.FC = () => { return () => clearInterval(interval); }, [loading, dispatch]); + /* ── Plan execution elapsed timer ───────────────────────── */ + useEffect(() => { + if (!showProcessingPlanSpinner) { + setProcessingElapsedSeconds(0); + return; + } + + setProcessingElapsedSeconds(0); + const interval = setInterval(() => { + setProcessingElapsedSeconds((currentSeconds: number) => currentSeconds + 1); + }, 1000); + + return () => clearInterval(interval); + }, [showProcessingPlanSpinner]); + /* ── Initial plan load ──────────────────────────────────── */ useEffect(() => { if (!planId) { @@ -367,6 +408,8 @@ const PlanPage: React.FC = () => { showBufferingText={showBufferingText} agentMessages={agentMessages} showProcessingPlanSpinner={showProcessingPlanSpinner} + processingElapsedSeconds={processingElapsedSeconds} + processingStatusMessage={processingStatusMessage} showApprovalButtons={showApprovalButtons} processingApproval={processingApproval} handleApprovePlan={handleApprovePlan} From 883b0e2be6c175674da8d1d84ebddcd14cf99439 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 23 Jun 2026 16:06:52 +0530 Subject: [PATCH 2/2] commit --- .../common/ProcessingStatusIndicator.tsx | 15 +++-------- src/App/src/hooks/usePlanWebSocket.tsx | 20 ++++++--------- src/App/src/utils/index.ts | 2 +- src/App/src/utils/utils.tsx | 25 ++++++++++++++++++- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/App/src/components/common/ProcessingStatusIndicator.tsx b/src/App/src/components/common/ProcessingStatusIndicator.tsx index c53a90e19..25d03e36f 100644 --- a/src/App/src/components/common/ProcessingStatusIndicator.tsx +++ b/src/App/src/components/common/ProcessingStatusIndicator.tsx @@ -1,26 +1,17 @@ import { Spinner } from '@fluentui/react-components'; +import { formatElapsedTime } from '@/utils'; interface ProcessingStatusIndicatorProps { message: string; elapsedSeconds?: number; } -const formatElapsedTime = (elapsedSeconds: number): string => { - if (elapsedSeconds < 60) { - return `${elapsedSeconds}s`; - } - - const minutes = Math.floor(elapsedSeconds / 60); - const seconds = elapsedSeconds % 60; - return `${minutes}min ${seconds}sec`; -}; - const ProcessingStatusIndicator = ({ message, elapsedSeconds, }: ProcessingStatusIndicatorProps) => { - const showElapsedSuffix = Number.isFinite(elapsedSeconds) && (elapsedSeconds as number) > 0; - const elapsedSuffix = showElapsedSuffix ? ` (${formatElapsedTime(elapsedSeconds as number)})` : ''; + const showElapsedSuffix = typeof elapsedSeconds === 'number' && elapsedSeconds > 0; + const elapsedSuffix = showElapsedSuffix ? ` (${formatElapsedTime(elapsedSeconds)})` : ''; return (
number; } -const formatElapsedTime = (elapsedSeconds: number): string => { - if (elapsedSeconds < 60) { - return `${elapsedSeconds}s`; - } - - const minutes = Math.floor(elapsedSeconds / 60); - const seconds = elapsedSeconds % 60; - return `${minutes}min ${seconds}sec`; -}; - /** * Creates an AgentMessageResponse and persists it, then optionally reloads the task list. */ @@ -115,8 +106,12 @@ export function usePlanWebSocket({ const processingStartedAtRef = React.useRef(null); useEffect(() => { - if (showProcessingPlanSpinner && processingStartedAtRef.current === null) { - processingStartedAtRef.current = Date.now(); + if (showProcessingPlanSpinner) { + if (processingStartedAtRef.current === null) { + processingStartedAtRef.current = Date.now(); + } + } else { + processingStartedAtRef.current = null; } }, [showProcessingPlanSpinner]); @@ -282,7 +277,6 @@ export function usePlanWebSocket({ }; dispatch(addAgentMessage(errorAgent)); dispatch(planFailedFinal()); - dispatch(setShowProcessingPlanSpinner(false)); processingStartedAtRef.current = null; dispatch(setShowBufferingText(false)); dispatch(setSubmittingChatDisableInput(true)); diff --git a/src/App/src/utils/index.ts b/src/App/src/utils/index.ts index 2de953bd8..1e79c9760 100644 --- a/src/App/src/utils/index.ts +++ b/src/App/src/utils/index.ts @@ -8,7 +8,7 @@ * - agentIconUtils → agent-to-icon mapping */ -export { formatDate } from './utils'; +export { formatDate, formatElapsedTime } from './utils'; export { getErrorMessage, getErrorStyle } from './errorUtils'; export { formatErrorMessage, extractPlainAnswer, truncate } from './messageUtils'; export { diff --git a/src/App/src/utils/utils.tsx b/src/App/src/utils/utils.tsx index 3129bbd01..e7f9ff6df 100644 --- a/src/App/src/utils/utils.tsx +++ b/src/App/src/utils/utils.tsx @@ -67,4 +67,27 @@ export const formatDate = ( }); return formatted; -} \ No newline at end of file +} + +/** + * Formats an elapsed-time duration in seconds for display in processing + * indicators and completion messages. + * + * Examples: + * - 5 → "5s" + * - 59 → "59s" + * - 60 → "1min 0sec" + * - 75 → "1min 15sec" + * + * @param elapsedSeconds Non-negative integer seconds elapsed. + * @returns Human-readable elapsed-time string. + */ +export const formatElapsedTime = (elapsedSeconds: number): string => { + if (elapsedSeconds < 60) { + return `${elapsedSeconds}s`; + } + + const minutes = Math.floor(elapsedSeconds / 60); + const seconds = elapsedSeconds % 60; + return `${minutes}min ${seconds}sec`; +};