mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-24 03:09:26 +00:00
fix(client): restore the released composer text after answering
Submitting an answer from the expanded card never goes through collapse, so the ordinary unsent message stashed when the question was moved back was never handed over and its entry leaked. The resume success path now restores it in place of the reset when one is stashed, and drops the entry either way. Read through a ref because the callback settles long after the render that armed it.
This commit is contained in:
parent
05d53c5535
commit
f84750ef22
2 changed files with 60 additions and 1 deletions
|
|
@ -14,6 +14,15 @@ const mockSetAnswerDrafts = jest.fn((update: unknown) => {
|
|||
? (update as (current: Record<string, string>) => Record<string, string>)(mockAnswerDrafts)
|
||||
: (update as Record<string, string>);
|
||||
});
|
||||
let mockReleasedComposerText: Record<string, string> = {};
|
||||
const mockSetReleasedComposerText = jest.fn((update: unknown) => {
|
||||
mockReleasedComposerText =
|
||||
typeof update === 'function'
|
||||
? (update as (current: Record<string, string>) => Record<string, string>)(
|
||||
mockReleasedComposerText,
|
||||
)
|
||||
: (update as Record<string, string>);
|
||||
});
|
||||
const mockSetDraft = jest.fn();
|
||||
let mockSaveDrafts = false;
|
||||
let mockCollapsedIds: string[] = [];
|
||||
|
|
@ -50,6 +59,9 @@ jest.mock('recoil', () => ({
|
|||
if (state.key === 'askAnswerModeText') {
|
||||
return [mockAnswerDrafts, mockSetAnswerDrafts];
|
||||
}
|
||||
if (state.key === 'askAnswerModeReleasedComposerText') {
|
||||
return [mockReleasedComposerText, mockSetReleasedComposerText];
|
||||
}
|
||||
return [[], jest.fn()];
|
||||
},
|
||||
useRecoilValue: () => mockSaveDrafts,
|
||||
|
|
@ -82,6 +94,7 @@ describe('useAskAnswerMode', () => {
|
|||
mockSaveDrafts = false;
|
||||
mockCollapsedIds = [];
|
||||
mockAnswerDrafts = {};
|
||||
mockReleasedComposerText = {};
|
||||
mockGetComposerText.mockReturnValue('answer from A');
|
||||
});
|
||||
|
||||
|
|
@ -230,6 +243,29 @@ describe('useAskAnswerMode', () => {
|
|||
expect(mockAnswerDrafts).toEqual({ a1: 'answer from A', a2: 'answer from B' });
|
||||
});
|
||||
|
||||
it('hands the released composer text back when the answer is submitted', () => {
|
||||
/** Stashed by a prior expand: the question was collapsed, an ordinary
|
||||
* message was typed in the released composer, then the question was moved
|
||||
* back. Submitting from there never goes through `collapse`, so the
|
||||
* message has to be restored here or it dies with the question. */
|
||||
mockReleasedComposerText = { a1: 'an ordinary unsent message' };
|
||||
mockUseGetMessages.mockReturnValue({ data: liveAsk });
|
||||
mockSubmitAskAnswer.mockImplementation(
|
||||
(_actionId: string, _answer: string, options?: { onSuccess?: () => void }) => {
|
||||
options?.onSuccess?.();
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(() => useAskAnswerMode('conversation-1'));
|
||||
|
||||
act(() => {
|
||||
result.current.submitText('answer from A');
|
||||
});
|
||||
|
||||
expect(mockSetComposerText).toHaveBeenLastCalledWith('text', 'an ordinary unsent message');
|
||||
expect(mockResetComposer).not.toHaveBeenCalled();
|
||||
expect(mockReleasedComposerText).toEqual({});
|
||||
});
|
||||
|
||||
it('does not let a delayed answer success clear the composer or selection after navigation', () => {
|
||||
let finishAnswer: (() => void) | undefined;
|
||||
mockUseGetMessages.mockReturnValue({ data: liveAsk });
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
actionId: liveAsk?.actionId,
|
||||
formContext,
|
||||
};
|
||||
/** Read by the resume success callback, which settles long after the render
|
||||
* that armed it, so it must not close over a stale stash. */
|
||||
const releasedComposerTextRef = useRef(releasedComposerText);
|
||||
releasedComposerTextRef.current = releasedComposerText;
|
||||
const mountedRef = useRef(true);
|
||||
useEffect(() => {
|
||||
// Strict Mode runs setup, cleanup, then setup again in development; each
|
||||
|
|
@ -364,12 +368,30 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
delete next[submittedActionId];
|
||||
return next;
|
||||
});
|
||||
/** A successful answer never goes through `collapse`, so the released
|
||||
* text has to be handed back here too or the user's ordinary
|
||||
* message dies with the question. Restored in place of the reset
|
||||
* when one is stashed; the entry is dropped either way so the map
|
||||
* stays bounded. */
|
||||
const released = releasedComposerTextRef.current[submittedActionId];
|
||||
if (
|
||||
(consumedComposerText || (wasActive && saveDrafts)) &&
|
||||
currentScope.formContext?.getValues('text') === submittedComposerText
|
||||
) {
|
||||
currentScope.formContext.reset();
|
||||
if (released) {
|
||||
currentScope.formContext.setValue('text', released);
|
||||
} else {
|
||||
currentScope.formContext.reset();
|
||||
}
|
||||
}
|
||||
setReleasedComposerText((current) => {
|
||||
if (current[submittedActionId] == null) {
|
||||
return current;
|
||||
}
|
||||
const next = { ...current };
|
||||
delete next[submittedActionId];
|
||||
return next;
|
||||
});
|
||||
},
|
||||
});
|
||||
return true;
|
||||
|
|
@ -380,6 +402,7 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
active,
|
||||
saveDrafts,
|
||||
conversationId,
|
||||
setReleasedComposerText,
|
||||
formContext,
|
||||
submitAskAnswer,
|
||||
setSelected,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue