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); } }