Kaynağa Gözat

feat: support for stopping automatic scrolling to bottom and add "to bottom" button (#1656)

Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Shimada666 1 yıl önce
ebeveyn
işleme
73e180638e

+ 42 - 16
frontend/src/components/Jupyter.tsx

@@ -1,10 +1,14 @@
-import React, { useEffect, useRef } from "react";
+import React, { useRef, useState } from "react";
 import { useSelector } from "react-redux";
 import SyntaxHighlighter from "react-syntax-highlighter";
 import Markdown from "react-markdown";
 import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
+import { VscArrowDown } from "react-icons/vsc";
+import { useTranslation } from "react-i18next";
 import { RootState } from "#/store";
 import { Cell } from "#/state/jupyterSlice";
+import { useScrollToBottom } from "#/hooks/useScrollToBottom";
+import { I18nKey } from "#/i18n/declaration";
 
 interface IJupyterCell {
   cell: Cell;
@@ -75,27 +79,49 @@ function JupyterCell({ cell }: IJupyterCell): JSX.Element {
 }
 
 function Jupyter(): JSX.Element {
+  const { t } = useTranslation();
+
   const { cells } = useSelector((state: RootState) => state.jupyter);
   const jupyterRef = useRef<HTMLDivElement>(null);
 
-  function scrollDomToBottom() {
-    const dom = jupyterRef.current;
-    if (dom) {
-      requestAnimationFrame(() => {
-        dom.scrollTo(0, dom.scrollHeight);
-      });
-    }
-  }
+  const [hitBottom, setHitBottom] = useState(true);
+  const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(jupyterRef);
+
+  const onChatBodyScroll = (e: HTMLElement) => {
+    const bottomHeight = e.scrollTop + e.clientHeight;
 
-  useEffect(() => {
-    scrollDomToBottom();
-  });
+    const isHitBottom = bottomHeight >= e.scrollHeight - 10;
+
+    setHitBottom(isHitBottom);
+    setAutoScroll(isHitBottom);
+  };
 
   return (
-    <div className="flex-1 overflow-y-auto flex flex-col" ref={jupyterRef}>
-      {cells.map((cell, index) => (
-        <JupyterCell key={index} cell={cell} />
-      ))}
+    <div className="flex-1">
+      <div
+        className="overflow-y-auto h-full"
+        ref={jupyterRef}
+        onScroll={(e) => onChatBodyScroll(e.currentTarget)}
+      >
+        {cells.map((cell, index) => (
+          <JupyterCell key={index} cell={cell} />
+        ))}
+      </div>
+      {!hitBottom && (
+        <div className="sticky bottom-2 flex items-center justify-center">
+          <button
+            type="button"
+            className="relative border-1 text-sm rounded px-3 py-1 border-neutral-600 bg-neutral-700 cursor-pointer select-none"
+          >
+            <span className="flex items-center" onClick={scrollDomToBottom}>
+              <VscArrowDown className="inline mr-2 w-3 h-3" />
+              <span className="inline-block" onClick={scrollDomToBottom}>
+                {t(I18nKey.CHAT_INTERFACE$TO_BOTTOM)}
+              </span>
+            </span>
+          </button>
+        </div>
+      )}
     </div>
   );
 }

+ 30 - 0
frontend/src/hooks/useScrollToBottom.ts

@@ -0,0 +1,30 @@
+import { RefObject, useEffect, useState } from "react";
+
+export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement>) {
+  // for auto-scroll
+
+  const [autoScroll, setAutoScroll] = useState(true);
+  function scrollDomToBottom() {
+    const dom = scrollRef.current;
+    if (dom) {
+      requestAnimationFrame(() => {
+        setAutoScroll(true);
+        dom.scrollTo({ top: dom.scrollHeight, behavior: "auto" });
+      });
+    }
+  }
+
+  // auto scroll
+  useEffect(() => {
+    if (autoScroll) {
+      scrollDomToBottom();
+    }
+  });
+
+  return {
+    scrollRef,
+    autoScroll,
+    setAutoScroll,
+    scrollDomToBottom,
+  };
+}

+ 4 - 0
frontend/src/i18n/translation.json

@@ -353,6 +353,10 @@
     "fr": "Assistant",
     "tr": "Gönder"
   },
+  "CHAT_INTERFACE$TO_BOTTOM": {
+    "en": "To Bottom",
+    "zh-CN": "回到底部"
+  },
   "SETTINGS$MODEL_TOOLTIP": {
     "en": "Select the language model to use.",
     "zh-CN": "选择要使用的语言模型",