Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/pages/AskPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,38 @@ export default function AskPage() {
setAskLoading(false);
};

// Leaving the page aborts the request. The in-flight handler then sees a new
// request id and will not clear the flags, so do it here or Ask comes back
// stuck on Typing with the composer disabled. A partial answer is kept.
const leaveCleanupRef = useRef({
abortActiveRequest,
addAskMessage,
setAskLoading,
setAskStreamingAssistant,
stoppedNote: t('ask.stopped', 'You stopped this answer before it finished.'),
});
leaveCleanupRef.current = {
abortActiveRequest,
addAskMessage,
setAskLoading,
setAskStreamingAssistant,
stoppedNote: t('ask.stopped', 'You stopped this answer before it finished.'),
};
useEffect(() => () => {
abortActiveRequest();
}, [abortActiveRequest]);
const cleanup = leaveCleanupRef.current;
const partial = useSessionStore.getState().askStreamingAssistant;
cleanup.abortActiveRequest();
cleanup.setAskLoading(false);
cleanup.setAskStreamingAssistant(null);
const kept = partial?.content.trim();
if (!kept) return;
cleanup.addAskMessage({
id: partial?.id,
role: 'assistant',
content: kept,
output: { response: kept, followUpQuestions: [], uncertainties: [cleanup.stoppedNote] },
});
}, []);

// A reply still in flight belongs to the scope it was asked in; a new debate
// or one-off mode starts a fresh conversation, so drop the late answer.
Expand Down
18 changes: 18 additions & 0 deletions src/test/pages/AskAccountSwitch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,22 @@ describe('Ask across an account change', () => {
expect(screen.queryByText(/Account A private answer/)).not.toBeInTheDocument();
expect(useSessionStore.getState().askStreamingAssistant).toBeNull();
});

it('does not stay on Typing after you leave mid-answer', async () => {
mocks.userId = 'user-a';
enableOneOffMode();
useSessionStore.getState().clearAskMessages();
const view = render(<MemoryRouter initialEntries={['/app/ask']}><AskPage /></MemoryRouter>);
fireEvent.change(screen.getByPlaceholderText('Ask your debate coach...'), { target: { value: 'How do I weigh cost?' } });
fireEvent.click(screen.getByRole('button', { name: 'Send message' }));
await waitFor(() => expect(mocks.live).toBeDefined());
act(() => mocks.live!.onDraft({ response: 'A partial answer' }));
expect(useSessionStore.getState().askLoading).toBe(true);

view.unmount();

expect(useSessionStore.getState().askLoading).toBe(false);
expect(useSessionStore.getState().askStreamingAssistant).toBeNull();
expect(useSessionStore.getState().askMessages.map((message) => message.content)).toContain('A partial answer');
});
});
Loading