mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-24 03:09:26 +00:00
fix(client): keep grouped markers and folded search media visible
Two things a fold could swallow. A detached-subagent projection is an empty THINK part flagged `reasoning_unavailable`, which `Part` renders as a `ReasoningMarker`. `ToolCallGroup` sent every THINK part to `ReasoningCompact`, which has no text to show and returns null, so the marker disappeared the moment its call joined a group. Those parts go back through the standalone renderer. A phase hoists its span's media out of the fold, and the nested segment renders with `hideAttachments` so a `WebSearch` row inside it stands down for that hoist. The hoist was `AttachmentGroup` alone, which drops `web_search` attachments outright, so a folded search lost its images, products and places instead of lifting them out. The card renders `SearchVerticals` beside the attachment row, mirroring `ToolCallGroup`.
This commit is contained in:
parent
638a55635c
commit
9441da45c3
4 changed files with 84 additions and 3 deletions
|
|
@ -13,6 +13,7 @@ import {
|
|||
import useSmoothStreaming from '~/hooks/Messages/useSmoothStreaming';
|
||||
import { getActivityLabelText } from '~/utils/activityLabels';
|
||||
import { ROW_GLYPH_SLOT, TOOL_ROW_CLASSES } from './rows';
|
||||
import SearchVerticals from './verticals';
|
||||
import { AttachmentGroup } from './Parts';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -285,9 +286,17 @@ export default function ActivityPhaseGroup({
|
|||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
/** `AttachmentGroup` drops `web_search` attachments, and the nested segment
|
||||
* renders with `hideAttachments` so its own `WebSearch` row stands down
|
||||
* for this hoist — so without `SearchVerticals` here a phase containing a
|
||||
* search would swallow its images, products and places instead of lifting
|
||||
* them out of the fold. Mirrors `ToolCallGroup`'s own hoist. */
|
||||
const media =
|
||||
attachments != null && attachments.length > 0 ? (
|
||||
<AttachmentGroup attachments={attachments} />
|
||||
<>
|
||||
<SearchVerticals attachments={attachments} />
|
||||
<AttachmentGroup attachments={attachments} />
|
||||
</>
|
||||
) : null;
|
||||
if (!label) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -658,6 +658,20 @@ export default function ToolCallGroup({
|
|||
if (part.type === ContentTypes.THINK) {
|
||||
const think = part.think;
|
||||
const reasoning = typeof think === 'string' ? think : (think?.value ?? '');
|
||||
/** A detached-subagent projection carries an empty THINK
|
||||
* part flagged `reasoning_unavailable`, which `Part`
|
||||
* renders as a `ReasoningMarker`. `ReasoningCompact` has
|
||||
* no text to show and returns null, so the marker has to
|
||||
* keep going through the standalone path or it vanishes
|
||||
* the moment its call joins a group. */
|
||||
if (reasoning.trim() === '' && part.reasoning_unavailable === true) {
|
||||
return renderPart(
|
||||
part,
|
||||
idx,
|
||||
isLast && idx === lastContentIdx,
|
||||
handleToolExpand,
|
||||
);
|
||||
}
|
||||
const streaming = isSubmitting && idx === lastContentIdx;
|
||||
const isAfterTool =
|
||||
partIndex > 0 && parts[partIndex - 1]?.part.type === ContentTypes.TOOL_CALL;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ContentTypes } from 'librechat-data-provider';
|
||||
import { ContentTypes, Tools } from 'librechat-data-provider';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { TMessageContentParts } from 'librechat-data-provider';
|
||||
import type { TAttachment, TMessageContentParts } from 'librechat-data-provider';
|
||||
import { ROW_GLYPH_SLOT, TOOL_ROW_CLASSES } from '../rows';
|
||||
import ActivityPhaseGroup from '../ActivityPhaseGroup';
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ jest.mock('~/hooks', () => {
|
|||
const expandCollapse = jest.requireActual('~/hooks/Messages/useExpandCollapse');
|
||||
const lazyCollapseBody = jest.requireActual('~/hooks/Messages/useLazyCollapseBody');
|
||||
return {
|
||||
useLocalize: () => (key: string) => key,
|
||||
useExpandCollapse: expandCollapse.default,
|
||||
useLazyCollapseBody: lazyCollapseBody.default,
|
||||
EXPAND_TRANSITION: expandCollapse.EXPAND_TRANSITION,
|
||||
|
|
@ -40,6 +41,22 @@ const makeLabelPart = (
|
|||
const labelPart = makeLabelPart(LABEL);
|
||||
const nextLabelPart = makeLabelPart(NEXT_LABEL);
|
||||
|
||||
/** A completed web search inside the phase: `AttachmentGroup` discards the
|
||||
* `web_search` type, so only `SearchVerticals` can surface these. */
|
||||
const searchAttachment = {
|
||||
type: Tools.web_search,
|
||||
[Tools.web_search]: {
|
||||
images: [
|
||||
{
|
||||
title: 'A pictured result',
|
||||
imageUrl: 'https://example.com/pic.png',
|
||||
thumbnailUrl: 'https://example.com/thumb.png',
|
||||
link: 'https://example.com/page',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as TAttachment;
|
||||
|
||||
describe('ActivityPhaseGroup', () => {
|
||||
let frames: Array<FrameRequestCallback | undefined>;
|
||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||
|
|
@ -366,4 +383,21 @@ describe('ActivityPhaseGroup', () => {
|
|||
fireEvent.transitionEnd(screen.getByTestId('activity-phase-panel'));
|
||||
expect(screen.queryByTestId('phase-content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hoists a folded search call verticals out of the card', () => {
|
||||
/** The nested segment renders with `hideAttachments`, standing its own
|
||||
* `WebSearch` verticals down for this hoist, and `AttachmentGroup` drops
|
||||
* `web_search` attachments outright — so the card is the only surface
|
||||
* left for a folded search's images, products and places. */
|
||||
render(
|
||||
<ActivityPhaseGroup labelPart={labelPart} hasContent attachments={[searchAttachment]}>
|
||||
<div data-testid="phase-content" />
|
||||
</ActivityPhaseGroup>,
|
||||
);
|
||||
|
||||
const image = screen.getByRole('link', { name: 'A pictured result' });
|
||||
expect(image).toHaveAttribute('href', 'https://example.com/page');
|
||||
/** Outside the fold: collapsing the card must not take the media with it. */
|
||||
expect(screen.getByTestId('activity-phase-panel')).not.toContainElement(image);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -506,6 +506,30 @@ describe('ToolCallGroup image hoisting', () => {
|
|||
expect(screen.getByTestId('compact-reasoning')).toHaveAttribute('data-after-tool', 'true');
|
||||
});
|
||||
|
||||
it('delegates an unavailable-reasoning part to the standalone renderer', () => {
|
||||
/** A detached-subagent projection has no text to compact, so `Part`'s
|
||||
* `ReasoningMarker` is the only thing that stands for it. Rendering it
|
||||
* as a compact row drops the marker the moment the call joins a group. */
|
||||
const unavailablePart = {
|
||||
type: ContentTypes.THINK,
|
||||
[ContentTypes.THINK]: '',
|
||||
reasoning_unavailable: true,
|
||||
} as TMessageContentParts;
|
||||
|
||||
renderGroup({
|
||||
...baseProps,
|
||||
parts: [
|
||||
{ part: makePart('t1'), idx: 0 },
|
||||
{ part: unavailablePart, idx: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Fetch_image/ }));
|
||||
|
||||
expect(screen.getByTestId('inner-1')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('compact-reasoning')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('unmounts tool bodies after a collapsed group finishes transitioning', () => {
|
||||
renderGroup(baseProps);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue