mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-31 08:56:48 +00:00
fix: sync subagent and answer handoffs
This commit is contained in:
parent
43b1be085a
commit
4e3f15c683
2 changed files with 58 additions and 7 deletions
|
|
@ -1,10 +1,15 @@
|
|||
const mockSubmitAskAnswer = jest.fn();
|
||||
const mockResetComposer = jest.fn();
|
||||
const mockGetComposerText = jest.fn(() => 'answer from A');
|
||||
const mockSetComposerText = jest.fn();
|
||||
const mockSetCollapsedIds = jest.fn();
|
||||
const mockSetSelected = jest.fn();
|
||||
const mockSetChecked = jest.fn();
|
||||
const mockSetAnswerDraft = jest.fn();
|
||||
const mockSetDraft = jest.fn();
|
||||
let mockSaveDrafts = false;
|
||||
let mockCollapsedIds: string[] = [];
|
||||
let mockAnswerDraft = { actionId: null as string | null, text: '' };
|
||||
|
||||
jest.mock('~/data-provider', () => ({ useGetMessagesByConvoId: jest.fn() }));
|
||||
jest.mock('~/components/Chat/Messages/Content/ApprovalContext', () => ({
|
||||
|
|
@ -15,15 +20,20 @@ jest.mock('~/Providers', () => ({
|
|||
useOptionalChatFormContext: () => ({
|
||||
reset: mockResetComposer,
|
||||
getValues: mockGetComposerText,
|
||||
setValue: mockSetComposerText,
|
||||
}),
|
||||
}));
|
||||
jest.mock('~/utils', () => ({
|
||||
getAskAnswerDraftId: (id: string) => `draft-${id}`,
|
||||
morphTransition: (update: () => void) => update(),
|
||||
setDraft: (...args: unknown[]) => mockSetDraft(...args),
|
||||
}));
|
||||
jest.mock('recoil', () => ({
|
||||
atom: (cfg: unknown) => cfg,
|
||||
useRecoilState: (state: { key?: string }) => {
|
||||
if (state.key === 'askAnswerModeCollapsedActions') {
|
||||
return [mockCollapsedIds, mockSetCollapsedIds];
|
||||
}
|
||||
if (state.key === 'askAnswerModeSelection') {
|
||||
return [null, mockSetSelected];
|
||||
}
|
||||
|
|
@ -31,7 +41,7 @@ jest.mock('recoil', () => ({
|
|||
return [[], mockSetChecked];
|
||||
}
|
||||
if (state.key === 'askAnswerModeText') {
|
||||
return [{ actionId: null, text: '' }, mockSetAnswerDraft];
|
||||
return [mockAnswerDraft, mockSetAnswerDraft];
|
||||
}
|
||||
return [[], jest.fn()];
|
||||
},
|
||||
|
|
@ -55,6 +65,8 @@ describe('useAskAnswerMode', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSaveDrafts = false;
|
||||
mockCollapsedIds = [];
|
||||
mockAnswerDraft = { actionId: null, text: '' };
|
||||
mockGetComposerText.mockReturnValue('answer from A');
|
||||
});
|
||||
|
||||
|
|
@ -141,6 +153,34 @@ describe('useAskAnswerMode', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('restores the card answer into the composer when drafts are disabled', () => {
|
||||
mockCollapsedIds = ['a1'];
|
||||
mockAnswerDraft = { actionId: 'a1', text: 'answer edited in the card' };
|
||||
mockUseGetMessages.mockReturnValue({ data: liveAsk });
|
||||
const { result } = renderHook(() => useAskAnswerMode('conversation-1'));
|
||||
|
||||
act(() => result.current.expand());
|
||||
|
||||
expect(mockSetComposerText).toHaveBeenCalledWith('text', 'answer edited in the card');
|
||||
});
|
||||
|
||||
it('keeps the ask-specific draft current while editing the card', () => {
|
||||
mockSaveDrafts = true;
|
||||
mockUseGetMessages.mockReturnValue({ data: liveAsk });
|
||||
const { result } = renderHook(() => useAskAnswerMode('conversation-1'));
|
||||
|
||||
act(() => result.current.setAnswerText('answer edited in the card'));
|
||||
|
||||
expect(mockSetAnswerDraft).toHaveBeenCalledWith({
|
||||
actionId: 'a1',
|
||||
text: 'answer edited in the card',
|
||||
});
|
||||
expect(mockSetDraft).toHaveBeenCalledWith({
|
||||
id: 'draft-a1',
|
||||
value: 'answer edited in the card',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not let a delayed answer success clear the composer or selection after navigation', () => {
|
||||
let finishAnswer: (() => void) | undefined;
|
||||
mockUseGetMessages.mockReturnValue({ data: liveAsk });
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
findLiveAskUserQuestion,
|
||||
splitOtherOption,
|
||||
} from '~/utils/approval';
|
||||
import { getAskAnswerDraftId, morphTransition } from '~/utils';
|
||||
import { getAskAnswerDraftId, morphTransition, setDraft } from '~/utils';
|
||||
import { useGetMessagesByConvoId } from '~/data-provider';
|
||||
import { useOptionalChatFormContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
|
@ -159,9 +159,15 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
(text: string) => {
|
||||
if (liveAsk) {
|
||||
setAnswerDraft({ actionId: liveAsk.actionId, text });
|
||||
/** While the card owns the answer, `useAutoSave` is tracking the
|
||||
* conversation draft instead. Keep the dormant ask draft current so
|
||||
* expanding can restore this edit without clobbering that message. */
|
||||
if (saveDrafts) {
|
||||
setDraft({ id: getAskAnswerDraftId(liveAsk.actionId), value: text });
|
||||
}
|
||||
}
|
||||
},
|
||||
[liveAsk, setAnswerDraft],
|
||||
[liveAsk, saveDrafts, setAnswerDraft],
|
||||
);
|
||||
|
||||
/** Selection state is per-question: a new pause must never inherit a stale
|
||||
|
|
@ -190,11 +196,16 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
|
||||
const expand = useCallback(() => {
|
||||
if (liveAsk) {
|
||||
morphTransition(() =>
|
||||
setCollapsedIds((prev) => prev.filter((id) => id !== liveAsk.actionId)),
|
||||
);
|
||||
morphTransition(() => {
|
||||
/** Autosave restores the ask-specific draft after the key switch. If
|
||||
* drafts are disabled, perform that handoff directly. */
|
||||
if (!saveDrafts) {
|
||||
formContext?.setValue('text', answerText);
|
||||
}
|
||||
setCollapsedIds((prev) => prev.filter((id) => id !== liveAsk.actionId));
|
||||
});
|
||||
}
|
||||
}, [liveAsk, setCollapsedIds]);
|
||||
}, [liveAsk, saveDrafts, formContext, answerText, setCollapsedIds]);
|
||||
|
||||
/** Pure check toggle: the keyboard highlight is steered only by the
|
||||
* composer's digit/arrow shortcuts, so a mouse toggle never leaves a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue