diff --git a/src/frontend/src/components/common/SessionTimeoutDialog.tsx b/src/frontend/src/components/common/SessionTimeoutDialog.tsx new file mode 100644 index 000000000..bb0235d88 --- /dev/null +++ b/src/frontend/src/components/common/SessionTimeoutDialog.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { + Dialog, + DialogSurface, + DialogTitle, + DialogContent, + DialogBody, + DialogActions, + Button, +} from '@fluentui/react-components'; +import { Clock20Regular } from '@fluentui/react-icons'; +import "../../styles/Panel.css"; + +interface SessionTimeoutDialogProps { + isOpen: boolean; + onGoHome: () => void; +} + +/** + * Non-dismissible dialog shown when the backend plan approval session times out. + * No close button, no outside click dismiss, no escape key dismiss. + */ +const SessionTimeoutDialog: React.FC = ({ + isOpen, + onGoHome, +}) => { + return ( + { + // Prevent any dismiss action (escape key, outside click) + }} + > + + + +
+ + Session Timed Out +
+
+ + Session timed out. Please go back to the home page. + + + + +
+
+
+ ); +}; + +export default SessionTimeoutDialog; diff --git a/src/frontend/src/models/enums.tsx b/src/frontend/src/models/enums.tsx index 6f0deac9a..5ecda2430 100644 --- a/src/frontend/src/models/enums.tsx +++ b/src/frontend/src/models/enums.tsx @@ -253,6 +253,7 @@ export enum WebsocketMessageType { USER_CLARIFICATION_REQUEST = "user_clarification_request", USER_CLARIFICATION_RESPONSE = "user_clarification_response", FINAL_RESULT_MESSAGE = "final_result_message", + TIMEOUT_NOTIFICATION = "timeout_notification", ERROR_MESSAGE = 'error_message' } diff --git a/src/frontend/src/pages/PlanPage.tsx b/src/frontend/src/pages/PlanPage.tsx index 11f598525..9b564dab2 100644 --- a/src/frontend/src/pages/PlanPage.tsx +++ b/src/frontend/src/pages/PlanPage.tsx @@ -20,6 +20,7 @@ import { APIService } from "../api/apiService"; import { StreamMessage, StreamingPlanUpdate } from "../models"; import { usePlanCancellationAlert } from "../hooks/usePlanCancellationAlert"; import PlanCancellationDialog from "../components/common/PlanCancellationDialog"; +import SessionTimeoutDialog from "../components/common/SessionTimeoutDialog"; import "../styles/PlanPage.css" // Create API service instance @@ -75,6 +76,9 @@ const PlanPage: React.FC = () => { const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null); const [cancellingPlan, setCancellingPlan] = useState(false); + // Session timeout dialog state + const [showSessionTimeoutDialog, setShowSessionTimeoutDialog] = useState(false); + const [loadingMessage, setLoadingMessage] = useState(loadingMessages[0]); // Plan cancellation alert hook @@ -443,6 +447,19 @@ const PlanPage: React.FC = () => { return () => unsubscribe(); }, [scrollToBottom, showToast, formatErrorMessage]); + // WebsocketMessageType.TIMEOUT_NOTIFICATION + useEffect(() => { + const unsubscribe = webSocketService.on(WebsocketMessageType.TIMEOUT_NOTIFICATION, (timeoutMessage: any) => { + console.log('⏰ Timeout notification received:', timeoutMessage); + setShowSessionTimeoutDialog(true); + setShowProcessingPlanSpinner(false); + setShowBufferingText(false); + webSocketService.disconnect(); + }); + + return () => unsubscribe(); + }, []); + //WebsocketMessageType.AGENT_MESSAGE useEffect(() => { const unsubscribe = webSocketService.on(WebsocketMessageType.AGENT_MESSAGE, (agentMessage: any) => { @@ -838,6 +855,15 @@ const PlanPage: React.FC = () => { onCancel={handleCancelDialog} loading={cancellingPlan} /> + + {/* Session Timeout Dialog */} + { + setShowSessionTimeoutDialog(false); + navigate('/'); + }} + /> ); }; diff --git a/src/frontend/src/services/WebSocketService.tsx b/src/frontend/src/services/WebSocketService.tsx index dbbf9137f..97b532262 100644 --- a/src/frontend/src/services/WebSocketService.tsx +++ b/src/frontend/src/services/WebSocketService.tsx @@ -247,6 +247,11 @@ class WebSocketService { this.emit(WebsocketMessageType.ERROR_MESSAGE, message.data); // Emit the data break; } + case WebsocketMessageType.TIMEOUT_NOTIFICATION: { + console.log("Received TIMEOUT_NOTIFICATION:", message); + this.emit(WebsocketMessageType.TIMEOUT_NOTIFICATION, message); + break; + } case WebsocketMessageType.USER_CLARIFICATION_RESPONSE: case WebsocketMessageType.REPLAN_APPROVAL_REQUEST: case WebsocketMessageType.REPLAN_APPROVAL_RESPONSE: