diff --git a/frontend/features/chat/components/sections/chat-area.tsx b/frontend/features/chat/components/sections/chat-area.tsx index 53fa6025..f3566d41 100644 --- a/frontend/features/chat/components/sections/chat-area.tsx +++ b/frontend/features/chat/components/sections/chat-area.tsx @@ -33,6 +33,7 @@ import { MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, + useMessageScroller, } from "@/components/ui/message-scroller"; import { ChatMessagePositionRail, @@ -43,6 +44,38 @@ import { AppLogo, DeeixLogo } from "@/shared/components/app-logo"; import { useBranding } from "@/shared/config/branding-provider"; import { PoweredByDeeix } from "@/shared/components/powered-by-deeix"; +function ScrollToPendingUser({ scrollKey }: { scrollKey: string }) { + const handledScrollKeyRef = React.useRef(""); + const { scrollToEnd } = useMessageScroller(); + + React.useLayoutEffect(() => { + if (!scrollKey) { + handledScrollKeyRef.current = ""; + return; + } + if (handledScrollKeyRef.current === scrollKey) { + return; + } + + handledScrollKeyRef.current = scrollKey; + let secondFrameID: number | null = null; + const firstFrameID = window.requestAnimationFrame(() => { + secondFrameID = window.requestAnimationFrame(() => { + const reducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; + scrollToEnd({ behavior: reducedMotion ? "auto" : "smooth" }); + }); + }); + return () => { + window.cancelAnimationFrame(firstFrameID); + if (secondFrameID !== null) { + window.cancelAnimationFrame(secondFrameID); + } + }; + }, [scrollKey, scrollToEnd]); + + return null; +} + function CompactDivider({ summaryPreview }: { summaryPreview: string }) { const t = useTranslations("chat.messages"); const [expanded, setExpanded] = React.useState(false); @@ -511,6 +544,10 @@ export function ChatArea({ } return ""; }, [hasLiveMessage, messages]); + const pendingUserScrollKey = React.useMemo( + () => [...messages].reverse().find((item) => item.role === "user" && item.isPending)?.key ?? "", + [messages], + ); return ( <> @@ -570,6 +607,7 @@ export function ChatArea({
+ { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 144, - sideOffset: 8, - verticalChrome: 40, - }), - 80, - ); -}); - -test("uses all normal viewport space remaining after panel chrome", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 424, - sideOffset: 8, - verticalChrome: 40, - }), - 360, - ); -}); - -test("limits a positioned submenu to its remaining viewport space", () => { - assert.equal(resolveDesktopMenuListMaxHeight(72, 12), 60); -}); - -test("uses the larger space below the trigger and deducts the side offset", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 424, - triggerTop: 96, - triggerBottom: 124, - sideOffset: 8, - verticalChrome: 40, - }), - 252, - ); -}); - -test("uses the larger space above the trigger", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 424, - triggerTop: 324, - triggerBottom: 352, - sideOffset: 8, - verticalChrome: 40, - }), - 252, - ); -}); - -test("returns zero when panel chrome consumes all available space", () => { - assert.equal( - resolveDesktopModelMenuListMaxHeight({ - viewportTop: 24, - viewportBottom: 64, - sideOffset: 8, - verticalChrome: 40, - }), - 0, - ); -}); diff --git a/frontend/package.json b/frontend/package.json index f62f19de..d75894a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,6 @@ "analyze:output": "next experimental-analyze --output", "start": "next start", "typecheck": "tsc --noEmit --pretty false", - "test": "node --no-warnings --test features/chat/components/sections/chat-model-picker-layout.test.mjs", "check": "pnpm lint && pnpm typecheck", "lint": "biome lint .", "lint:fix": "biome lint --write .",