diff --git a/app/globals.css b/app/globals.css index 53dbf7a5..ac45a87e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -687,6 +687,25 @@ animation: rise-in 240ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards; } +/* + * A bot's reply that came in while the chat was open: all of it at once, up from a touch smaller and a soft blur, + * settling fast (Grok Bot's message rows: 12px, 94%, opaque by halfway, cubic-bezier(.23, 1, .32, 1)). + */ +@keyframes reply-in { + from { + opacity: 0; + filter: blur(4px); + transform: translateY(12px) scale(0.94); + } + 55% { + opacity: 1; + } +} +.motion-reply-in { + transform-origin: left bottom; + animation: reply-in 320ms cubic-bezier(0.23, 1, 0.32, 1) backwards; +} + /* What you just sent: up from the composer, a touch smaller at first. */ @keyframes send-in { from { @@ -850,6 +869,7 @@ @media (prefers-reduced-motion: reduce) { .motion-rise-in, + .motion-reply-in, .motion-send-in, .motion-new-item, .motion-menu-in, diff --git a/components/app/chat-view.tsx b/components/app/chat-view.tsx index 16bef6e9..0d592fc6 100644 --- a/components/app/chat-view.tsx +++ b/components/app/chat-view.tsx @@ -1412,7 +1412,12 @@ function MessageRow({ m, state, showSender, continued, sessionById, chipIds, onO // Decided once, as the row mounts: a poll or a new tapback draws it again but never replays it. // Yours carries on its pending copy's motion (a negative delay starts it partway, or past the end). const [enter] = useState(() => - !arrival ? undefined : arrival.kind === "send" ? { className: "motion-send-in", style: arrival.since ? { animationDelay: `${arrival.since - Date.now()}ms` } : undefined } : { className: "motion-rise-in", style: undefined }, + !arrival + ? undefined + : arrival.kind === "send" + ? { className: "motion-send-in", style: arrival.since ? { animationDelay: `${arrival.since - Date.now()}ms` } : undefined } + : // A bot's reply arrives whole, with an entrance of its own; anything else that comes in rises in. + { className: m.role === "bot" ? "motion-reply-in" : "motion-rise-in", style: undefined }, ); const enterClass = enter?.className ?? ""; // What was already here when the row mounted sits still: its tapbacks.