diff --git a/apps/web/src/controllers/ReasoningController/ReasoningScrollToBottom.tsx b/apps/web/src/components/ui/buttons/ScrollToBottomButton.tsx
similarity index 77%
rename from apps/web/src/controllers/ReasoningController/ReasoningScrollToBottom.tsx
rename to apps/web/src/components/ui/buttons/ScrollToBottomButton.tsx
index 923579c8a..b094d6f24 100644
--- a/apps/web/src/controllers/ReasoningController/ReasoningScrollToBottom.tsx
+++ b/apps/web/src/components/ui/buttons/ScrollToBottomButton.tsx
@@ -3,18 +3,20 @@ import { ChevronDown } from '@/components/ui/icons';
import { AppTooltip } from '@/components/ui/tooltip';
import { cn } from '@/lib/classMerge';
-export const ReasoningScrollToBottom: React.FC<{
+export const ScrollToBottomButton: React.FC<{
isAutoScrollEnabled: boolean;
scrollToBottom: () => void;
-}> = React.memo(({ isAutoScrollEnabled, scrollToBottom }) => {
+ className?: string;
+}> = React.memo(({ isAutoScrollEnabled, scrollToBottom, className }) => {
return (
);
-}
+});
+
+EditorContainer.displayName = 'EditorContainer';
diff --git a/apps/web/src/components/ui/report/ReportEditor.tsx b/apps/web/src/components/ui/report/ReportEditor.tsx
index 73bc7b34d..9e6f66da9 100644
--- a/apps/web/src/components/ui/report/ReportEditor.tsx
+++ b/apps/web/src/components/ui/report/ReportEditor.tsx
@@ -2,8 +2,9 @@
import type { Value, AnyPluginConfig } from 'platejs';
import { Plate, type TPlateEditor } from 'platejs/react';
-import React, { useImperativeHandle, useRef } from 'react';
+import React, { useEffect, useImperativeHandle, useRef } from 'react';
import { useDebounceFn, useMemoizedFn } from '@/hooks';
+import { useAutoScroll } from '@/hooks/useAutoScroll';
import { cn } from '@/lib/utils';
import { Editor } from './Editor';
import { EditorContainer } from './EditorContainer';
@@ -12,6 +13,7 @@ import { useReportEditor } from './useReportEditor';
import type { ReportElementsWithIds, ReportElementWithId } from '@buster/server-shared/reports';
import { platejsToMarkdown } from './plugins/markdown-kit/platejs-conversions';
import { ShimmerText } from '@/components/ui/typography/ShimmerText';
+import { ScrollToBottomButton } from '../buttons/ScrollToBottomButton';
interface ReportEditorProps {
// We accept the generic Value type but recommend using ReportTypes.Value for type safety
@@ -29,7 +31,7 @@ interface ReportEditorProps {
onReady?: (editor: IReportEditor) => void;
id?: string;
mode?: 'export' | 'default';
- children?: React.ReactNode;
+ preEditorChildren?: React.ReactNode;
postEditorChildren?: React.ReactNode;
}
@@ -59,13 +61,21 @@ export const ReportEditor = React.memo(
useFixedToolbarKit = false,
readOnly = false,
isStreaming = false,
- children,
+ preEditorChildren,
postEditorChildren
},
ref
) => {
// Initialize the editor instance using the custom useEditor hook
const isReady = useRef(false);
+ const editorContainerRef = useRef(null);
+
+ const { isAutoScrollEnabled, enableAutoScroll, disableAutoScroll, scrollToBottom } =
+ useAutoScroll(editorContainerRef, {
+ enabled: isStreaming,
+ bottomThreshold: 50,
+ observeSubTree: true
+ });
const editor = useReportEditor({
isStreaming,
@@ -118,15 +128,24 @@ export const ReportEditor = React.memo(
wait: 1500
});
+ useEffect(() => {
+ if (isStreaming) {
+ enableAutoScroll();
+ } else {
+ disableAutoScroll();
+ }
+ }, [isStreaming]);
+
if (!editor) return null;
return (
- {children}
+ {preEditorChildren}
{postEditorChildren}
+ {isStreaming && (
+
+ )}
);
diff --git a/apps/web/src/components/ui/report/elements/StreamingText.tsx b/apps/web/src/components/ui/report/elements/StreamingText.tsx
index 6cabd1964..8b2b1f675 100644
--- a/apps/web/src/components/ui/report/elements/StreamingText.tsx
+++ b/apps/web/src/components/ui/report/elements/StreamingText.tsx
@@ -15,7 +15,7 @@ export function StreamingText(props: PlateTextProps) {
= ({ chatId
-
diff --git a/apps/web/src/controllers/ReportPageControllers/ReportPageController.tsx b/apps/web/src/controllers/ReportPageControllers/ReportPageController.tsx
index 2702b552c..e5badb834 100644
--- a/apps/web/src/controllers/ReportPageControllers/ReportPageController.tsx
+++ b/apps/web/src/controllers/ReportPageControllers/ReportPageController.tsx
@@ -10,9 +10,7 @@ import { type IReportEditor } from '@/components/ui/report/ReportEditor';
import { ReportEditorSkeleton } from '@/components/ui/report/ReportEditorSkeleton';
import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
import { useTrackAndUpdateReportChanges } from '@/api/buster-electric/reports/hooks';
-import { ShimmerText } from '@/components/ui/typography/ShimmerText';
import { GeneratingContent } from './GeneratingContent';
-import { useHotkeys } from 'react-hotkeys-hook';
export const ReportPageController: React.FC<{
reportId: string;
@@ -73,19 +71,20 @@ export const ReportPageController: React.FC<{
mode={mode}
onReady={onReadyProp}
isStreaming={isStreamingMessage}
+ preEditorChildren={
+
+ }
postEditorChildren={
showGeneratingContent ? (
) : null
- }>
-
-
+ }>
) : (
)}
diff --git a/apps/web/src/styles/tailwindAnimations.css b/apps/web/src/styles/tailwindAnimations.css
index ff9a9458b..0a86ace6c 100644
--- a/apps/web/src/styles/tailwindAnimations.css
+++ b/apps/web/src/styles/tailwindAnimations.css
@@ -60,21 +60,16 @@
}
}
-/*
- highlightFade animation:
- - Animates a highlight background and a bottom border only (no outline).
- - The border is only on the bottom, not using outline.
-*/
@keyframes highlightFade {
0% {
/* Use highlight background, fallback to brand, then yellow */
background-color: var(--color-highlight-background, var(--color-purple-100, yellow));
- /* Only bottom border is visible at start */
- border-bottom: 1px solid var(--color-highlight-border, var(--color-purple-200, yellow));
+ /* Use box-shadow instead of border - doesn't take up space */
+ box-shadow: 0 1.5px 0 0 var(--color-highlight-border, var(--color-purple-300, yellow));
}
100% {
background-color: var(--color-highlight-to-background, transparent);
- border-bottom: 0px solid var(--color-highlight-to-border, transparent);
+ box-shadow: 0 0 0 0 var(--color-highlight-to-border, transparent);
}
}