mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-01 03:27:01 +00:00
🪄 feat: Smooth Activity Phase Transitions (#14832)
* feat: Animate activity phase transitions
* style: Match activity phase formatting
* 🪄 fix: Fold activity phase entrance in one direction, flush-left label
The phase header replaced <summary> with <button>, which brought the UA
`text-align: center` with it — the label span is `flex-1`, so the text
filled the row and centered inside it. Left-align it and drop the leading
glyph: the card's border and fill already carry the weight, and the child
tool groups keep their own icons.
The entrance also read as two movements. The card, header and inset all
hard-cut in at full size, displacing the transcript below by ~57px, then
folded back up past the header that had just pushed it down. The card now
mounts in the shape of what was already on screen — zero-height header,
transparent chrome, no inset — and grows the header as the panel collapses,
so the block's height only ever decreases. Chrome, padding and both heights
share one curve.
The collapse also waits for a painted start value; a single rAF can land
before paint, and a start value the compositor never saw snaps rather than
transitions.
- Restore the e2e parent-phase selectors, which still matched `summary`
- Memoize the hoisted `groupActivityPhases` pass and its phase-index set
- Finish the amber -> `text-text-warning` sweep in ToolCallGroup and Part
* 🩹 fix: Scope phase-entrance history and resolve media queries at mount
Addresses both Codex findings on #14832.
`MultiMessage` renders siblings without a key, so `ContentParts` survives a
sibling switch with its refs intact. The recorded phase-marker set outlived
the message it described, and any phase in the newly selected sibling whose
index was absent from the previous sibling's set was read as a live arrival —
already-loaded history mounted expanded and collapsed itself. Scope the set
to its messageId and treat a mismatch as a fresh mount.
`useMediaQuery` initialized to `false` and resolved only in a passive effect,
so the first render always reported "no match". Anything branching once at
mount — the frozen entrance flag here, and every other first-paint decision
across its call sites — never saw the correction, which is how a
`prefers-reduced-motion: reduce` user still got the fold. Read the query
synchronously in the state initializer and guard both paths for environments
without `matchMedia`.
* ♿ fix: Honor reduced motion on manual phase disclosure
The entrance already respected the preference, but manually opening or
closing a phase did not: `useExpandCollapse` writes its transition as an
inline style, which cannot carry a `prefers-reduced-motion` media query,
and there is no global reduced-motion reset in the stylesheet. Before this
PR the phase used `<details>`, which had no animation at all — so the swap
to an animated disclosure handed reduced-motion readers a 300ms fold they
did not have.
Resolve the preference in the hook and drop the transition outright. Every
expanding panel in the message content shares it, so tool calls, thinking
blocks, attachments and web-search sources are covered by the same change.
The chevron and the fold's own utility classes get `motion-reduce`
overrides, which the inline styles cannot express.
* 🩹 fix: Keep the collapse completion signal under reduced motion
`transition: none` emits no `transitionend`, and ToolCallGroup waits on
that event to drop `shouldRenderBody`. Removing the transition therefore
left every collapsed tool subtree mounted indefinitely — expensive and
stateful children retained for exactly the readers who asked for less
work, not more.
Shorten the duration to 0.01ms instead. It is imperceptible, still fires
the event, and keeps the hook the single place that knows about the
preference. Caught by Codex on 3b9bd2181d.
This commit is contained in:
parent
c06fbff475
commit
5d3edeb383
11 changed files with 631 additions and 47 deletions
|
|
@ -214,7 +214,7 @@ test.describe('parent activity phases', () => {
|
|||
const finalTextIndex = content.findIndex((part) => contentPartText(part).includes(finalText));
|
||||
expect(finalTextIndex).toBe(phasePart?.activity_end_index);
|
||||
|
||||
const parent = messagesView(page).locator(`summary[aria-label="${PARENT_LABEL}"]`);
|
||||
const parent = messagesView(page).getByRole('button', { name: PARENT_LABEL, exact: true });
|
||||
await expect(parent).toBeVisible({ timeout: 30000 });
|
||||
await expect(messagesView(page).getByText(finalText)).toBeVisible({ timeout: 30000 });
|
||||
await parent.click();
|
||||
|
|
@ -241,7 +241,10 @@ test.describe('parent activity phases', () => {
|
|||
).toBe(true);
|
||||
|
||||
await page.reload();
|
||||
const reloadedParent = messagesView(page).locator(`summary[aria-label="${PARENT_LABEL}"]`);
|
||||
const reloadedParent = messagesView(page).getByRole('button', {
|
||||
name: PARENT_LABEL,
|
||||
exact: true,
|
||||
});
|
||||
await expect(reloadedParent).toBeVisible({ timeout: 30000 });
|
||||
await expect(messagesView(page).getByText(finalText)).toBeVisible();
|
||||
await reloadedParent.click();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue