From aada1b8b65cfa720d941a2a832c49bb677d3fefe Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 19:45:35 +0800 Subject: [PATCH] fix: observer chat message container width - Added useElementWidth hook to observe and track the width of a referenced element using ResizeObserver. - Updated ChatInterface and WelcomeScreen components to utilize the new hook for responsive layout adjustments. - Enhanced AIDisplayEntriesPart and AIDisplayFeedsPart components with improved styling for better alignment and spacing. Signed-off-by: Innei --- .../displays/AIDisplayEntriesPart.tsx | 4 +- .../displays/AIDisplayFeedsPart.tsx | 4 +- .../components/layouts/ChatInterface.tsx | 50 +++++++------------ .../components/layouts/WelcomeScreen.tsx | 15 +++++- packages/internal/hooks/src/index.ts | 1 + .../internal/hooks/src/useElementWidth.ts | 39 +++++++++++++++ 6 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 packages/internal/hooks/src/useElementWidth.ts diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx index 722429bde..29796f5c0 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx @@ -39,11 +39,11 @@ const SingleEntryCard = ({ entry }: { entry: SingleEntry }) => { target={undefined} siteUrl={entry.url ?? undefined} /> -

+

{entry.title || "Untitled Entry"}

- + ) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx index e6f52b2f1..ae0b2a001 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx @@ -40,11 +40,11 @@ const AIDisplayFeedPartBase = ({ }} siteUrl={siteUrl!} /> -

+

{title || "Untitled Feed"}

- + {/* Description */} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index dba6d14cd..fa729a119 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -1,5 +1,6 @@ import { useFocusable } from "@follow/components/common/Focusable/hooks.js" import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" +import { useElementWidth } from "@follow/hooks" import { getCategoryFeedIds } from "@follow/store/subscription/getter" import { usePrefetchSummary } from "@follow/store/summary/hooks" import { tracker } from "@follow/tracker" @@ -10,7 +11,7 @@ import type { EditorState, LexicalEditor } from "lexical" import { AnimatePresence } from "motion/react" import { nanoid } from "nanoid" import type { FC, RefObject } from "react" -import { startTransition, Suspense, use, useEffect, useLayoutEffect, useRef, useState } from "react" +import { Suspense, use, useEffect, useLayoutEffect, useRef, useState } from "react" import { useEventCallback, useEventListener } from "usehooks-ts" import { useAISettingKey } from "~/atoms/settings/ai" @@ -391,26 +392,8 @@ export const ChatInterface = (props: ChatInterfaceProps) => ( const Messages: FC<{ contentRef?: RefObject }> = ({ contentRef }) => { const messages = useMessages() - - const [messageContainerWidth, setMessageContainerWidth] = useState(0) - - useLayoutEffect(() => { - if (!contentRef) return - - const setMessageContainerWidthTransition = () => - startTransition(() => { - setMessageContainerWidth(contentRef.current?.clientWidth ?? 0) - }) - setMessageContainerWidthTransition() - const resizeObserver = new ResizeObserver(() => { - setMessageContainerWidthTransition() - }) - resizeObserver.observe(contentRef.current) - - return () => { - resizeObserver.disconnect() - } - }, [contentRef]) + const fallbackRef = useRef(null) + const messageContainerWidth = useElementWidth(contentRef ?? fallbackRef) return (
}> = ({ contentRef } } as React.CSSProperties } > - {messages.map((message, index) => { - const isLastMessage = index === messages.length - 1 - return ( - - {message.role === "user" ? ( - - ) : ( - - )} - - ) - })} + {!!messageContainerWidth && + messages.map((message, index) => { + const isLastMessage = index === messages.length - 1 + return ( + + {message.role === "user" ? ( + + ) : ( + + )} + + ) + })}
) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx index df60a28d4..2e5cb79d9 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx @@ -1,6 +1,7 @@ import { Folo } from "@follow/components/icons/folo.js" import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" import { FeedViewType } from "@follow/constants" +import { useElementWidth } from "@follow/hooks" import { clsx } from "@follow/utils" import type { EditorState, LexicalEditor } from "lexical" import { AnimatePresence, m } from "motion/react" @@ -174,6 +175,9 @@ const TimelineSummarySection = () => { const { t } = useTranslation("ai") + const messageContainerRef = useRef(null) + const messageContainerWidth = useElementWidth(messageContainerRef) + const isLoading = status === "streaming" const isError = status === "error" return ( @@ -196,7 +200,16 @@ const TimelineSummarySection = () => { )} -
+
0 ? 1 : 0, + } as React.CSSProperties + } + > {isError ? (

{t("timeline_summary.error")}

) : hasContent && message ? ( diff --git a/packages/internal/hooks/src/index.ts b/packages/internal/hooks/src/index.ts index 4b6200bb9..97402522c 100644 --- a/packages/internal/hooks/src/index.ts +++ b/packages/internal/hooks/src/index.ts @@ -4,6 +4,7 @@ export * from "./useAnyPointDown" export * from "./useControlled" export * from "./useCountDown" export * from "./useDark" +export * from "./useElementWidth" export * from "./useInputComposition" export * from "./useInterval" export * from "./useIsOnline" diff --git a/packages/internal/hooks/src/useElementWidth.ts b/packages/internal/hooks/src/useElementWidth.ts new file mode 100644 index 000000000..8592c8b3e --- /dev/null +++ b/packages/internal/hooks/src/useElementWidth.ts @@ -0,0 +1,39 @@ +import type { RefObject } from "react" +import { startTransition, useLayoutEffect, useState } from "react" + +/** + * Hook to track the width of an element using ResizeObserver + * @param ref - RefObject pointing to the element to observe + * @returns The current width of the element (0 if element is not available) + */ +export function useElementWidth(ref: RefObject): number { + const [width, setWidth] = useState(0) + + useLayoutEffect(() => { + if (!ref.current) return + + const updateWidth = (newWidth: number) => { + startTransition(() => { + setWidth(newWidth) + }) + } + + // Set initial width + updateWidth(ref.current.clientWidth) + + // Create ResizeObserver to track width changes + const resizeObserver = new ResizeObserver(() => { + if (ref.current) { + updateWidth(ref.current.clientWidth) + } + }) + + resizeObserver.observe(ref.current) + + return () => { + resizeObserver.disconnect() + } + }, [ref]) + + return width +}