From 283b31e78609f4d329848fc16ecbe1f202dc2c04 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Mon, 23 Mar 2026 19:02:15 +0000 Subject: [PATCH] Fix AI mode: race condition, split-brain UI, and stuck local state - Backend: set status to ai_mode in the route handler before submitting to AI runner, eliminating the race condition where frontend's immediate status poll read the old status - Frontend: replace all raw isAiModeActive prop usages with effectiveAiModeActive in DiscussionPanel (13 locations) so ReasoningPanel, status text, loading indicator, and manual/AI controls all reflect the correct state instantly on Start AI Mode click - Frontend: add useEffect to sync localAiModeActive back to null once the parent prop catches up, preventing permanent override after natural session end - These fixes also unblock the 3-second AI message polling which was never activating due to isAiModeActive staying false Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routes/focus_group_ai.py | 15 +++++++- .../focus-group-session/DiscussionPanel.tsx | 37 +++++++++++-------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/backend/app/routes/focus_group_ai.py b/backend/app/routes/focus_group_ai.py index ed6613ed..3750c16a 100755 --- a/backend/app/routes/focus_group_ai.py +++ b/backend/app/routes/focus_group_ai.py @@ -757,7 +757,20 @@ async def start_autonomous_conversation(focus_group_id): if not ai_runner.is_running: current_app.logger.error("AI Runner service is not running") return jsonify({"error": "AI Runner service is not available"}), 503 - + + # Set status to ai_mode NOW (before submitting to background runner) so that + # the frontend's immediate status poll after this response reads the correct state. + # The controller will also set it, making this idempotent. + from datetime import datetime, timezone + try: + await FocusGroup.update(focus_group_id, { + 'status': 'ai_mode', + 'autonomous_started_at': datetime.now(timezone.utc) + }) + current_app.logger.info("Set focus group status to ai_mode before AI runner submission") + except Exception as e: + current_app.logger.warning(f"Failed to pre-set ai_mode status: {e}") + # Submit the conversation to the AI Runner (non-blocking) current_app.logger.info("Submitting conversation to AI Runner...") try: diff --git a/src/components/focus-group-session/DiscussionPanel.tsx b/src/components/focus-group-session/DiscussionPanel.tsx index 77dc5f13..dffb6101 100755 --- a/src/components/focus-group-session/DiscussionPanel.tsx +++ b/src/components/focus-group-session/DiscussionPanel.tsx @@ -77,12 +77,19 @@ const DiscussionPanel = ({ // Calculate reasoning panel visibility - only show when user explicitly expands it const reasoningPanelVisible = reasoningPanelExpanded; + // Sync localAiModeActive back to null once parent prop has caught up + useEffect(() => { + if (localAiModeActive !== null && localAiModeActive === isAiModeActive) { + setLocalAiModeActive(null); + } + }, [isAiModeActive, localAiModeActive]); + // Fetch reasoning history when in AI mode useEffect(() => { - if (isAiModeActive && focusGroupId) { + if (effectiveAiModeActive && focusGroupId) { checkAutonomousStatus(); } - }, [isAiModeActive, focusGroupId]); + }, [effectiveAiModeActive, focusGroupId]); // Check autonomous conversation status const checkAutonomousStatus = async () => { @@ -91,7 +98,7 @@ const DiscussionPanel = ({ try { // Status is managed by parent component through isAiModeActive prop // Just fetch reasoning history if AI mode is active - if (isAiModeActive) { + if (effectiveAiModeActive) { fetchReasoningHistory(); } } catch (error) { @@ -123,21 +130,21 @@ const DiscussionPanel = ({ // Polling for reasoning history and status when AI mode is active useEffect(() => { let interval: NodeJS.Timeout; - - if (isAiModeActive && focusGroupId) { + + if (effectiveAiModeActive && focusGroupId) { // Poll more frequently for reasoning updates and status sync (every 5 seconds) interval = setInterval(() => { fetchReasoningHistory(); checkAutonomousStatus(); // Also check if status has changed }, 5000); } - + return () => { if (interval) { clearInterval(interval); } }; - }, [isAiModeActive, focusGroupId]); + }, [effectiveAiModeActive, focusGroupId]); // Initialize message count reference useEffect(() => { @@ -1176,17 +1183,17 @@ const DiscussionPanel = ({ /> ) ))} - {(isTyping || isAiModeActive) && ( + {(isTyping || effectiveAiModeActive) && (
- {isAiModeActive ? ( + {effectiveAiModeActive ? ( ) : ( )}
- {isAiModeActive ? 'AI is generating next response...' : 'Generating AI response...'} + {effectiveAiModeActive ? 'AI is generating next response...' : 'Generating AI response...'}
)} @@ -1217,7 +1224,7 @@ const DiscussionPanel = ({ reasoningHistory={reasoningHistory} isVisible={reasoningPanelVisible} onToggle={() => setReasoningPanelExpanded(!reasoningPanelExpanded)} - isAiMode={isAiModeActive} + isAiMode={effectiveAiModeActive} /> {/* Control panel - pinned to bottom */} @@ -1292,7 +1299,7 @@ const DiscussionPanel = ({

{isSpeaking ? 'Speaking...' : - isAiModeActive ? 'AI mode active' : + effectiveAiModeActive ? 'AI mode active' : 'Manual moderation mode'}

@@ -1312,7 +1319,7 @@ const DiscussionPanel = ({ {autonomousLoading ? ( <> - {isAiModeActive ? 'Stopping...' : 'Starting...'} + {effectiveAiModeActive ? 'Stopping...' : 'Starting...'} ) : effectiveAiModeActive ? ( <> @@ -1348,7 +1355,7 @@ const DiscussionPanel = ({
{/* Show manual controls only when not in AI mode */} - {!isAiModeActive && ( + {!effectiveAiModeActive && ( <>