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 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-23 19:02:15 +00:00
parent b4978989a5
commit 283b31e786
2 changed files with 36 additions and 16 deletions

View file

@ -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:

View file

@ -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) && (
<div className="flex items-center space-x-2 text-sm text-slate-500 animate-pulse">
<div className="bg-primary/10 p-2 rounded-full">
{isAiModeActive ? (
{effectiveAiModeActive ? (
<Bot className="h-4 w-4 text-primary animate-spin" />
) : (
<MessageCircle className="h-4 w-4 text-primary" />
)}
</div>
<span>
{isAiModeActive ? 'AI is generating next response...' : 'Generating AI response...'}
{effectiveAiModeActive ? 'AI is generating next response...' : 'Generating AI response...'}
</span>
</div>
)}
@ -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 = ({
<div className="flex items-center space-x-2">
<p className="text-sm text-slate-500">
{isSpeaking ? 'Speaking...' :
isAiModeActive ? 'AI mode active' :
effectiveAiModeActive ? 'AI mode active' :
'Manual moderation mode'}
</p>
@ -1312,7 +1319,7 @@ const DiscussionPanel = ({
{autonomousLoading ? (
<>
<Bot className="mr-1 h-3 w-3 animate-spin" />
{isAiModeActive ? 'Stopping...' : 'Starting...'}
{effectiveAiModeActive ? 'Stopping...' : 'Starting...'}
</>
) : effectiveAiModeActive ? (
<>
@ -1348,7 +1355,7 @@ const DiscussionPanel = ({
<div className="flex items-center gap-2">
{/* Show manual controls only when not in AI mode */}
{!isAiModeActive && (
{!effectiveAiModeActive && (
<>
<Button
variant="outline"
@ -1376,7 +1383,7 @@ const DiscussionPanel = ({
)}
{/* Show AI mode status and controls */}
{isAiModeActive && (
{effectiveAiModeActive && (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 text-sm text-slate-600">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>