Skip to content

Commit dc2b80a

Browse files
authored
Merge pull request #2965 from OtterMind/fix/ai-stream-status-settle
fix(ai): settle the stream status when a request ends without a terminal callback
2 parents 8d2285d + 014d3c6 commit dc2b80a

2 files changed

Lines changed: 19 additions & 0 deletions

File tree

‎chat2db-community-client/src/blocks/AI/index.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,8 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession
550550
const messagesRef = useRef<IChatItem[]>([]);
551551
const currentRoundUserMessageIdRef = useRef<string | null>(null);
552552
const statusRef = useRef<SSERequestStatus>(SSERequestStatus.IDLE);
553+
// Kept in a ref because the chunk handler is memoized without the request controls as dependencies.
554+
const stopRequestRef = useRef<(() => void) | null>(null);
553555
const inProgressSessionRef = useRef<IInProgressSessionSnapshot | null>(null);
554556
const chatInputRef = useRef<ChatInputPropsRef>(null);
555557
const messageListRef = useRef<HTMLDivElement>(null);
@@ -867,6 +869,9 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession
867869
}
868870
}
869871
feedback.error(chunk.content || 'AI stream error');
872+
// The error already arrived, so end the generation: otherwise the request stays in LOADING and the
873+
// model logo keeps spinning next to the error message.
874+
stopRequestRef.current?.();
870875
}
871876
}, []);
872877

@@ -879,6 +884,10 @@ export default function AI({ variant = 'page', onTableClick, onPinSql, onSession
879884
undefined,
880885
);
881886

887+
useEffect(() => {
888+
stopRequestRef.current = stop;
889+
}, [stop]);
890+
882891
// Load the model list.
883892

884893
const loadModelOptions = useCallback(async () => {

‎chat2db-community-client/src/hooks/useSSERequest.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const useSSERequest = <T = string>(
7575
let accumulatedContent = '';
7676
let requestHandle: SSERequestHandle | undefined;
7777
let callbackHandledError = false;
78+
let terminalCallbackFired = false;
7879
const isActiveRequest = () => mountedRef.current && requestOwner.owns(generation, requestHandle);
7980

8081
try {
@@ -83,10 +84,12 @@ const useSSERequest = <T = string>(
8384
guardSSERequestCallbacks<SSEOutput>(
8485
{
8586
onSuccess: () => {
87+
terminalCallbackFired = true;
8688
console.log('[useSSERequest] onSuccess, chunks count:', chunks.length);
8789
setStatus(SSERequestStatus.FINISH);
8890
},
8991
onError: (err) => {
92+
terminalCallbackFired = true;
9093
callbackHandledError = true;
9194
console.log('[useSSERequest] onError:', err);
9295
setStatus(SSERequestStatus.ERROR);
@@ -117,6 +120,7 @@ const useSSERequest = <T = string>(
117120
}
118121
},
119122
onStop: () => {
123+
terminalCallbackFired = true;
120124
console.log('[useSSERequest] onStop');
121125
setStatus(SSERequestStatus.FINISH);
122126
},
@@ -136,6 +140,12 @@ const useSSERequest = <T = string>(
136140
setStatus(SSERequestStatus.ERROR);
137141
setError(error_ instanceof Error ? error_ : new Error('Unknown error'));
138142
} finally {
143+
if (!terminalCallbackFired && mountedRef.current && requestOwner.isCurrentGeneration(generation)) {
144+
// A stream can end without a terminal callback, for example when the backend answers with a
145+
// business error payload. Never leave the request in LOADING, otherwise the UI keeps showing
146+
// its generating state next to an error that already arrived.
147+
setStatus(SSERequestStatus.ERROR);
148+
}
139149
if (requestHandle) {
140150
requestOwner.release(generation, requestHandle);
141151
}

0 commit comments

Comments
 (0)