assistant-ui icon indicating copy to clipboard operation
assistant-ui copied to clipboard

ThreadViewport autoScroll behavior is broken

Open vaniyokk opened this issue 7 months ago • 1 comments

I have an issue with autoScroll prop.

I think it's wrong to prevent user to scroll up when response is generated. There is no way now to preserve sroll to bottom button functionality and disable autoscroll when response is generated.

useThreadViewportAutoScroll.ts

  const scrollToBottom = (behavior: ScrollBehavior) => {
    const div = divRef.current;
    if (!div || !autoScroll) return;

    isScrollingToBottomRef.current = true;
    div.scrollTo({ top: div.scrollHeight, behavior });
  };

if autoScroll is true, there is no way to scroll up when response is generated. if autoScroll is false, scroll to bottom never happened, even when scroll to bottom button clicked.

vaniyokk avatar May 02 '25 09:05 vaniyokk

I've overridden default behavior by flipping autoScroll prop based on isRunning and container scroll state. It allows me to scroll UP while response is generated, but ThreadScrollToBottom control still not working due to code above. I added custom onClick handler to set my own autoScroll to true and it seems to work properly now. Probably this should be fixed on library level.

const isRunning = useThread(thread => thread.isRunning);
  const [autoScroll, setAutoScroll] = useState(true);
  const lastScrollTop = useRef<number>(0);

  useEffect(() => {
    setAutoScroll(true);
  }, [isRunning]);

  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
    const target = event.target as HTMLDivElement;

    if (isRunning && lastScrollTop.current > target.scrollTop) {
      setAutoScroll(false);
    }

    lastScrollTop.current = target.scrollTop;
  };

  return (
    <ThreadPrimitive.Root
      className={cn("bg-background box-border flex h-full flex-col", className)}
      style={{
        ["--thread-max-width" as string]: "42rem"
      }}
    >
      <ThreadPrimitive.Viewport
        className="flex h-full flex-col items-center overflow-y-auto scroll-smooth bg-inherit px-4 pt-4 lg:pt-8"
        autoScroll={autoScroll}
        onScroll={handleScroll}
      >
      ...
      <div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit pb-4">
          <ThreadScrollToBottom onClick={() => setAutoScroll(true)} />
          <TrialAlert />
          <Composer onClickInput={onClickInput} />
        </div>

vaniyokk avatar May 02 '25 09:05 vaniyokk

Hi, I can't reproduce this, auto scroll is working as expected for me. Please check again, and if you're still experiencing this provide a minimal reproduction

AVGVSTVS96 avatar May 31 '25 03:05 AVGVSTVS96

Hi, I can't reproduce this, auto scroll is working as expected for me. Please check again, and if you're still experiencing this provide a minimal reproduction

To clarify, I am able to scroll up when a message is streaming to stop the auto scrolling, scrolling back down re-engages auto scrolling.

Closing as stale for now, please report if anyone is experiencing this

AVGVSTVS96 avatar Jun 05 '25 20:06 AVGVSTVS96

@AVGVSTVS96 just checked on a fresh installation. It's working fine with OpenAI runtime, but with ExternalStoreRuntime, it's not. Now sure how to properly demonstrate you, but just switching the runtime to ExternalStoreRuntime makes chat not scrollable up while response is streaming...

Do you have an idea why is the difference in behavior based on the runtime provider?

vaniyokk avatar Jun 10 '25 13:06 vaniyokk

@AVGVSTVS96 just checked on a fresh installation. It's working fine with OpenAI runtime, but with ExternalStoreRuntime, it's not. Now sure how to properly demonstrate you, but just switching the runtime to ExternalStoreRuntime makes chat not scrollable up while response is streaming...

Do you have an idea why is the difference in behavior based on the runtime provider?

Will look into this today, thanks for reporting

AVGVSTVS96 avatar Jun 10 '25 13:06 AVGVSTVS96

@AVGVSTVS96 thanks! Hope it'll reproduce on your end. I checked source code for any potential problems and didn't found anything.

In case anyone need temporary solution


import { useThread, useThreadViewport } from "@assistant-ui/react";
import { useEffect, useRef, useState } from "react";

/**
 * Custom hook to manage autoscroll logic for chat thread.
 * Returns [autoScroll, setAutoScroll, handleScroll].
 */
export function useThreadAutoScrollFix() {
  const isRunning = useThread(thread => thread.isRunning);
  const isAtBottom = useThreadViewport(thread => thread.isAtBottom);
  const [autoScroll, setAutoScroll] = useState(true);
  const lastScrollTop = useRef<number>(0);

  useEffect(() => {
    setAutoScroll(true);
  }, [isRunning]);

  useEffect(() => {
    if (isRunning && isAtBottom) {
      setAutoScroll(true);
    }
  }, [isRunning, isAtBottom]);

  const handleClickAutoScroll = () => {
    setAutoScroll(true);
  };

  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
    const target = event.target as HTMLDivElement;
    if (isRunning && lastScrollTop.current > target.scrollTop) {
      setAutoScroll(false);
    }
    lastScrollTop.current = target.scrollTop;
  };

  return { autoScroll, handleClickAutoScroll, handleScroll };
}


export const Thread: FC<{
  agent?: Agent | null;
  className?: string;
  onClickInput?: (event: React.MouseEvent<HTMLElement>) => void;
}> = ({ agent, onClickInput, className }) => {
  const { autoScroll, handleClickAutoScroll, handleScroll } =
    useThreadAutoScrollFix();

  return (
    <ThreadPrimitive.Root
      className={cn("bg-background box-border flex h-full flex-col", className)}
      style={{
        ["--thread-max-width" as string]: "48rem"
      }}
    >
      <ThreadPrimitive.Viewport
        className="flex h-full flex-col items-center overflow-y-auto scroll-smooth bg-inherit px-4 pt-4 lg:pt-8"
        autoScroll={autoScroll}  // changed line here
        onScroll={handleScroll}  // changed line here
      >
        <ThreadWelcome />

        <ThreadPrimitive.Messages
          components={{
            UserMessage: UserMessage,
            EditComposer: EditComposer,
            AssistantMessage: AssistantMessage
          }}
        />

        <ThreadPrimitive.If empty={false}>
          <div className="min-h-8 flex-grow" />
        </ThreadPrimitive.If>

        <div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit pb-4">
          <ThreadScrollToBottom onClick={handleClickAutoScroll} />  // changed line here
          <Composer onClickInput={onClickInput} />
        </div>
      </ThreadPrimitive.Viewport>
    </ThreadPrimitive.Root>
  );
};

vaniyokk avatar Jun 10 '25 13:06 vaniyokk

Not sure if it helps, but uploaded video with proof that just switching runtime fixes issue.

https://github.com/user-attachments/assets/5445c73c-c793-4d2c-849f-4f13ce05b8b8

vaniyokk avatar Jun 10 '25 14:06 vaniyokk

This is a tough bug to track down! We're not sure why this is happening. For anyone experiencing this issue, please use the workaround in https://github.com/assistant-ui/assistant-ui/issues/1916#issuecomment-2959330183 for the time being.

AVGVSTVS96 avatar Jul 22 '25 00:07 AVGVSTVS96