mirror of https://github.com/buster-so/buster.git
better loading state for report
This commit is contained in:
parent
b8e4b7e70d
commit
2dbe578f3a
|
@ -421,9 +421,24 @@ const AppSplitterBase = forwardRef<
|
|||
const startSize = savedLayout ?? 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
// Frame-based animation for smoother performance
|
||||
// We'll track the expected frame count based on 60fps for timing reference
|
||||
const expectedFrameCount = Math.ceil((duration / 1000) * 60);
|
||||
let frameCount = 0;
|
||||
let lastFrameTime = startTime;
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
frameCount++;
|
||||
const deltaTime = currentTime - lastFrameTime;
|
||||
lastFrameTime = currentTime;
|
||||
|
||||
// Use frame-based progress as primary, with time-based as fallback
|
||||
// This ensures smooth animation even when frames are dropped
|
||||
const frameProgress = frameCount / expectedFrameCount;
|
||||
const timeProgress = (currentTime - startTime) / duration;
|
||||
|
||||
// Use the more conservative progress to prevent overshooting
|
||||
const progress = Math.min(Math.max(frameProgress, timeProgress), 1);
|
||||
const easedProgress = easeInOutCubic(progress);
|
||||
|
||||
const currentSize = startSize + (targetSize - startSize) * easedProgress;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { lazy, Suspense } from 'react';
|
||||
import { type UsePageReadyOptions, usePageReady } from '@/hooks/usePageReady';
|
||||
import type { ReportEditorProps } from './ReportEditor';
|
||||
import { ReportEditorSkeleton } from './ReportEditorSkeleton';
|
||||
|
||||
|
@ -10,7 +11,25 @@ const DynamicReportEditorBase = lazy(() =>
|
|||
})
|
||||
);
|
||||
|
||||
export const DynamicReportEditor = (props: ReportEditorProps) => {
|
||||
export interface DynamicReportEditorProps extends ReportEditorProps {
|
||||
loadingOptions?: UsePageReadyOptions;
|
||||
}
|
||||
|
||||
export const DynamicReportEditor = ({ loadingOptions, ...props }: DynamicReportEditorProps) => {
|
||||
const { delay = 200, idleTimeout = 500, forceImmediate = false } = loadingOptions || {};
|
||||
|
||||
const { isReady: isPageReady } = usePageReady({
|
||||
...loadingOptions,
|
||||
delay,
|
||||
idleTimeout,
|
||||
forceImmediate,
|
||||
});
|
||||
|
||||
// Show skeleton only when not ready (not during transition)
|
||||
if (!isPageReady) {
|
||||
return <ReportEditorSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ReportEditorSkeleton />}>
|
||||
<DynamicReportEditorBase {...props} />
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { useEffect, useState, useTransition } from 'react';
|
||||
|
||||
export interface UsePageReadyOptions {
|
||||
/**
|
||||
* Delay in milliseconds after initial mount before considering page ready
|
||||
* @default 25
|
||||
*/
|
||||
delay?: number;
|
||||
/**
|
||||
* Whether to wait for requestIdleCallback (when browser is idle)
|
||||
* @default true
|
||||
*/
|
||||
waitForIdle?: boolean;
|
||||
/**
|
||||
* Timeout for requestIdleCallback in milliseconds
|
||||
* @default 500
|
||||
*/
|
||||
idleTimeout?: number;
|
||||
/**
|
||||
* Whether to force immediate loading
|
||||
* @default false
|
||||
*/
|
||||
forceImmediate?: boolean;
|
||||
/**
|
||||
* Whether to use React's useTransition for non-urgent updates
|
||||
* @default true
|
||||
*/
|
||||
useTransition?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that determines when the page is ready for heavy component loading.
|
||||
* Uses React's useTransition to defer non-urgent updates and prevent blocking animations.
|
||||
*/
|
||||
export const usePageReady = (options: UsePageReadyOptions = {}) => {
|
||||
const {
|
||||
delay = 25,
|
||||
waitForIdle = true,
|
||||
idleTimeout = 500,
|
||||
forceImmediate = false,
|
||||
useTransition: useTransitionFlag = true,
|
||||
} = options;
|
||||
|
||||
const [isReady, setIsReady] = useState(forceImmediate);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
if (forceImmediate) {
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let idleCallbackId: number;
|
||||
|
||||
const markAsReady = () => {
|
||||
if (useTransitionFlag) {
|
||||
// Use transition to defer this non-urgent update
|
||||
startTransition(() => {
|
||||
setIsReady(true);
|
||||
});
|
||||
} else {
|
||||
setIsReady(true);
|
||||
}
|
||||
};
|
||||
|
||||
const checkReadiness = () => {
|
||||
if (waitForIdle && 'requestIdleCallback' in window) {
|
||||
// Wait for browser to be idle
|
||||
idleCallbackId = requestIdleCallback(markAsReady, { timeout: idleTimeout });
|
||||
} else {
|
||||
// Fallback: just use the delay
|
||||
markAsReady();
|
||||
}
|
||||
};
|
||||
|
||||
// Initial delay to let any animations or transitions settle
|
||||
timeoutId = setTimeout(() => {
|
||||
// Use requestAnimationFrame to ensure DOM is ready
|
||||
requestAnimationFrame(() => {
|
||||
checkReadiness();
|
||||
});
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (idleCallbackId) cancelIdleCallback(idleCallbackId);
|
||||
};
|
||||
}, [delay, waitForIdle, idleTimeout, forceImmediate, useTransitionFlag]);
|
||||
|
||||
return { isReady, isPending };
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import { ClientOnly } from '@tanstack/react-router';
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { ScrollToBottomButton } from '@/components/features/buttons/ScrollToBottomButton';
|
||||
import { useGetChatMessageIds } from '@/context/Chats';
|
||||
import { useAutoScroll } from '@/hooks/useAutoScroll';
|
||||
|
@ -14,6 +14,7 @@ const autoClass = 'mx-auto max-w-[600px] w-full';
|
|||
export const ChatContent: React.FC<{ chatId: string | undefined }> = React.memo(({ chatId }) => {
|
||||
const chatMessageIds = useGetChatMessageIds(chatId);
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const { isAutoScrollEnabled, scrollToBottom, enableAutoScroll } = useAutoScroll(containerRef, {
|
||||
observeSubTree: true,
|
||||
|
@ -27,31 +28,38 @@ export const ChatContent: React.FC<{ chatId: string | undefined }> = React.memo(
|
|||
if (!container) return;
|
||||
containerRef.current = container;
|
||||
enableAutoScroll();
|
||||
setIsMounted(true);
|
||||
});
|
||||
|
||||
const showScrollToBottomButton = isMounted && containerRef.current;
|
||||
|
||||
return (
|
||||
<ClientOnly>
|
||||
<div className="mb-48 flex h-full w-full flex-col">
|
||||
{chatMessageIds?.map((messageId, index) => (
|
||||
<div key={messageId} className={autoClass}>
|
||||
<ChatMessageBlock
|
||||
key={messageId}
|
||||
messageId={messageId}
|
||||
chatId={chatId || ''}
|
||||
messageIndex={index}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<>
|
||||
<div className={cn('mb-48 flex h-full w-full flex-col', !isMounted && 'invisible')}>
|
||||
<ClientOnly>
|
||||
{chatMessageIds?.map((messageId, index) => (
|
||||
<div key={messageId} className={autoClass}>
|
||||
<ChatMessageBlock
|
||||
key={messageId}
|
||||
messageId={messageId}
|
||||
chatId={chatId || ''}
|
||||
messageIndex={index}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
<ChatInputWrapper>
|
||||
<ScrollToBottomButton
|
||||
isAutoScrollEnabled={isAutoScrollEnabled}
|
||||
scrollToBottom={scrollToBottom}
|
||||
className="absolute -top-10"
|
||||
/>
|
||||
{showScrollToBottomButton && (
|
||||
<ScrollToBottomButton
|
||||
isAutoScrollEnabled={isAutoScrollEnabled}
|
||||
scrollToBottom={scrollToBottom}
|
||||
className={'absolute -top-10'}
|
||||
/>
|
||||
)}
|
||||
</ChatInputWrapper>
|
||||
</ClientOnly>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue