mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 12:13:30 +00:00
🫧 refactor: Fading Pre-Response Dot, Legacy Cursor CSS Removed (#14772)
* 🫧 refactor: Fading Pre-Response Dot, Legacy Cursor CSS Removed The pre-response dot keeps its classic 12px shape but now enters on the streamed-text fade curve (lc-fade-in) and breathes on opacity, replacing the legacy pulseSize scale throb that read as jittery. One dot for all users — the interim result-thinking-fade variant and its per-component class wiring are removed, and the dot goes static under prefers-reduced-motion. Clears out the legacy cursor CSS while here: the scaleX/scale3d mixed keyframes, -webkit- prefixed animation/transform/backface duplicates, translateZ and will-change layer hacks, subpixel font-smoothing, the duplicate #0d0d0d fallback, the unused .blink utility, and all three duplicate @keyframes blink definitions (the auth logo uses Tailwind's logo-blink; nothing references these). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * 📝 docs: Explain the Pre-Response Dot's Custom-CSS Exception Per AGENTS.md, custom CSS needs a stated reason a shared primitive cannot express the requirement: the dot is a pseudo-element gated on an ancestor's `.submitting` state plus its own `:empty`, kept in CSS so it costs no React renders per streamed token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9980b6221f
commit
8f1f43f33e
4 changed files with 38 additions and 121 deletions
|
|
@ -6,7 +6,7 @@ import MarkdownErrorBoundary from './MarkdownErrorBoundary';
|
|||
import { FADE_HYDRATION_THRESHOLD } from './animate';
|
||||
import { useMessageContext } from '~/Providers';
|
||||
import MarkdownBlocks from './MarkdownBlocks';
|
||||
import { preprocessLaTeX, cn } from '~/utils';
|
||||
import { preprocessLaTeX } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
type TContentProps = {
|
||||
|
|
@ -50,12 +50,7 @@ const Markdown = memo(function Markdown({ content = '', isLatestMessage }: TCont
|
|||
return (
|
||||
<div className="absolute">
|
||||
<p className="relative">
|
||||
<span
|
||||
className={cn(
|
||||
isLatestMessage && 'result-thinking',
|
||||
isLatestMessage && smoothStreaming && 'result-thinking-fade',
|
||||
)}
|
||||
/>
|
||||
<span className={isLatestMessage ? 'result-thinking' : ''} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,21 +27,17 @@ const parseThinkingContent = (text: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
const LoadingFallback = () => {
|
||||
const smoothStreaming = useSmoothStreaming();
|
||||
|
||||
return (
|
||||
<div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-visible">
|
||||
<div className="markdown prose dark:prose-invert light w-full break-words">
|
||||
<div className="absolute">
|
||||
<p className="submitting relative">
|
||||
<span className={cn('result-thinking', smoothStreaming && 'result-thinking-fade')} />
|
||||
</p>
|
||||
</div>
|
||||
const LoadingFallback = () => (
|
||||
<div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-visible">
|
||||
<div className="markdown prose dark:prose-invert light w-full break-words">
|
||||
<div className="absolute">
|
||||
<p className="submitting relative">
|
||||
<span className="result-thinking" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
const ErrorBox = ({
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import { memo } from 'react';
|
||||
import useSmoothStreaming from '~/hooks/Messages/useSmoothStreaming';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
/** Streaming cursor placeholder — no bottom margin to match Container's structure and prevent CLS */
|
||||
const EmptyTextPart = memo(() => {
|
||||
const smoothStreaming = useSmoothStreaming();
|
||||
|
||||
return (
|
||||
<div className="text-message flex min-h-[20px] flex-col items-start gap-3 overflow-visible">
|
||||
<div className="markdown prose dark:prose-invert light w-full break-words">
|
||||
<div className="absolute">
|
||||
<p className="submitting relative">
|
||||
<span className={cn('result-thinking', smoothStreaming && 'result-thinking-fade')} />
|
||||
<span className="result-thinking" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -640,39 +640,6 @@ pre {
|
|||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blink 1s linear infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
79% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.prose {
|
||||
color: var(--tw-prose-body);
|
||||
max-width: 65ch;
|
||||
|
|
@ -1642,16 +1609,6 @@ html {
|
|||
text-decoration-line: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.hidden-visibility {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
@ -1719,49 +1676,46 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulseSize {
|
||||
/* Pre-response dot: enters on the streamed-text fade curve, then breathes
|
||||
on opacity — replaces the legacy scale throb so waiting and text arrival
|
||||
share one motion language.
|
||||
|
||||
Custom CSS exception: this indicator is a `::after` pseudo-element on the
|
||||
`.result-thinking` hook, shown only while an ancestor carries `.submitting`
|
||||
and the hook itself is `:empty`. No `@librechat/client` primitive can
|
||||
express "render only while an ancestor is in state X and I have no
|
||||
children" — a component would have to own that state, which the streaming
|
||||
markdown path deliberately keeps in the DOM so the dot costs no React
|
||||
renders per token. Scoped to the one selector, colored from
|
||||
`--text-primary` so both themes follow, and disabled under
|
||||
prefers-reduced-motion. */
|
||||
@keyframes lc-dot-breathe {
|
||||
0%,
|
||||
to {
|
||||
-webkit-transform: scaleX(1);
|
||||
transform: scaleX(1);
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale3d(1.25, 1.25, 1);
|
||||
transform: scale3d(1.25, 1.25, 1);
|
||||
}
|
||||
}
|
||||
@keyframes pulseSize {
|
||||
0%,
|
||||
to {
|
||||
-webkit-transform: scaleX(1);
|
||||
transform: scaleX(1);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale3d(1.25, 1.25, 1);
|
||||
transform: scale3d(1.25, 1.25, 1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
.submitting .result-thinking:empty:last-child:after {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
-webkit-animation: pulseSize 1.25s ease-in-out infinite;
|
||||
animation: pulseSize 1.25s ease-in-out infinite;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
background-color: #0d0d0d;
|
||||
animation:
|
||||
lc-fade-in 250ms ease-out both,
|
||||
lc-dot-breathe 1.5s ease-in-out 250ms infinite;
|
||||
background-color: rgb(var(--text-primary));
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 12px;
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-transform-origin: center;
|
||||
transform-origin: center;
|
||||
width: 12px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.submitting .result-thinking:empty:last-child:after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-stroke {
|
||||
|
|
@ -1930,30 +1884,6 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
/* Pre-first-token dot, restyled to match the word fade: it fades in on the
|
||||
same curve rather than popping, then breathes on opacity instead of the
|
||||
classic size throb, so "run starting" and "text arriving" read as one
|
||||
system. Only applied while the fade is active; without it the dot keeps
|
||||
its original pulseSize behavior. */
|
||||
@keyframes lc-dot-breathe {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.35;
|
||||
transform: translateZ(0) scale(0.85);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateZ(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.submitting .result-thinking.result-thinking-fade:empty:last-child:after {
|
||||
animation:
|
||||
lc-fade-in 250ms ease-out both,
|
||||
lc-dot-breathe 1.6s ease-in-out 250ms infinite;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.webkit-dark-styles,
|
||||
.webkit-dark-styles:focus {
|
||||
background-clip: content-box;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue