mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-28 03:27:40 +00:00
FEAT: improve conversation history loading for long sessions (#371)
* feat: add "Load all messages" button for long conversations Scrolling up through long conversations requires many "load more" cycles. This adds a "Load all messages" floating button that fetches the entire conversation history in one shot. - Floating overlay pill appears after each batch finishes loading, persists 2s - Shows loading spinner while fetching all messages - Shows green "All messages loaded" confirmation for 1s before disappearing - Preserves scroll position when bulk-loading (no viewport jump) - Ref-based guards prevent scroll handler from re-fetching after load-all - Performance warning shown; "Scroll to bottom" resets visible cap - Clean state reset on session switch - i18n keys for en and zh-CN Note: default page size (20) and visible cap (100) are unchanged. These could be increased in a follow-up or made configurable via settings. * re-implement load-all feature for new TS architecture
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
|
|
||||||
import { api, authenticatedFetch } from '../../../utils/api';
|
import { api, authenticatedFetch } from '../../../utils/api';
|
||||||
import type { ChatMessage, Provider } from '../types/types';
|
import type { ChatMessage, Provider } from '../types/types';
|
||||||
import type { Project, ProjectSession } from '../../../types/app';
|
import type { Project, ProjectSession } from '../../../types/app';
|
||||||
@@ -76,15 +77,22 @@ export function useChatSessionState({
|
|||||||
const [tokenBudget, setTokenBudget] = useState<Record<string, unknown> | null>(null);
|
const [tokenBudget, setTokenBudget] = useState<Record<string, unknown> | null>(null);
|
||||||
const [visibleMessageCount, setVisibleMessageCount] = useState(INITIAL_VISIBLE_MESSAGES);
|
const [visibleMessageCount, setVisibleMessageCount] = useState(INITIAL_VISIBLE_MESSAGES);
|
||||||
const [claudeStatus, setClaudeStatus] = useState<{ text: string; tokens: number; can_interrupt: boolean } | null>(null);
|
const [claudeStatus, setClaudeStatus] = useState<{ text: string; tokens: number; can_interrupt: boolean } | null>(null);
|
||||||
|
const [allMessagesLoaded, setAllMessagesLoaded] = useState(false);
|
||||||
|
const [isLoadingAllMessages, setIsLoadingAllMessages] = useState(false);
|
||||||
|
const [loadAllJustFinished, setLoadAllJustFinished] = useState(false);
|
||||||
|
const [showLoadAllOverlay, setShowLoadAllOverlay] = useState(false);
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isLoadingSessionRef = useRef(false);
|
const isLoadingSessionRef = useRef(false);
|
||||||
const isLoadingMoreRef = useRef(false);
|
const isLoadingMoreRef = useRef(false);
|
||||||
|
const allMessagesLoadedRef = useRef(false);
|
||||||
const topLoadLockRef = useRef(false);
|
const topLoadLockRef = useRef(false);
|
||||||
const pendingScrollRestoreRef = useRef<ScrollRestoreState | null>(null);
|
const pendingScrollRestoreRef = useRef<ScrollRestoreState | null>(null);
|
||||||
const pendingInitialScrollRef = useRef(true);
|
const pendingInitialScrollRef = useRef(true);
|
||||||
const messagesOffsetRef = useRef(0);
|
const messagesOffsetRef = useRef(0);
|
||||||
const scrollPositionRef = useRef({ height: 0, top: 0 });
|
const scrollPositionRef = useRef({ height: 0, top: 0 });
|
||||||
|
const loadAllFinishedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const loadAllOverlayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const createDiff = useMemo<DiffCalculator>(() => createCachedDiffCalculator(), []);
|
const createDiff = useMemo<DiffCalculator>(() => createCachedDiffCalculator(), []);
|
||||||
|
|
||||||
@@ -182,6 +190,15 @@ export function useChatSessionState({
|
|||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const scrollToBottomAndReset = useCallback(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
if (allMessagesLoaded) {
|
||||||
|
setVisibleMessageCount(INITIAL_VISIBLE_MESSAGES);
|
||||||
|
setAllMessagesLoaded(false);
|
||||||
|
allMessagesLoadedRef.current = false;
|
||||||
|
}
|
||||||
|
}, [allMessagesLoaded, scrollToBottom]);
|
||||||
|
|
||||||
const isNearBottom = useCallback(() => {
|
const isNearBottom = useCallback(() => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
@@ -196,6 +213,7 @@ export function useChatSessionState({
|
|||||||
if (!container || isLoadingMoreRef.current || isLoadingMoreMessages) {
|
if (!container || isLoadingMoreRef.current || isLoadingMoreMessages) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (allMessagesLoadedRef.current) return false;
|
||||||
if (!hasMoreMessages || !selectedSession || !selectedProject) {
|
if (!hasMoreMessages || !selectedSession || !selectedProject) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -245,23 +263,24 @@ export function useChatSessionState({
|
|||||||
const nearBottom = isNearBottom();
|
const nearBottom = isNearBottom();
|
||||||
setIsUserScrolledUp(!nearBottom);
|
setIsUserScrolledUp(!nearBottom);
|
||||||
|
|
||||||
const scrolledNearTop = container.scrollTop < 100;
|
if (!allMessagesLoadedRef.current) {
|
||||||
if (!scrolledNearTop) {
|
const scrolledNearTop = container.scrollTop < 100;
|
||||||
topLoadLockRef.current = false;
|
if (!scrolledNearTop) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topLoadLockRef.current) {
|
|
||||||
// After a top-load restore, release the lock once user has moved away from absolute top.
|
|
||||||
if (container.scrollTop > 20) {
|
|
||||||
topLoadLockRef.current = false;
|
topLoadLockRef.current = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const didLoad = await loadOlderMessages(container);
|
if (topLoadLockRef.current) {
|
||||||
if (didLoad) {
|
if (container.scrollTop > 20) {
|
||||||
topLoadLockRef.current = true;
|
topLoadLockRef.current = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const didLoad = await loadOlderMessages(container);
|
||||||
|
if (didLoad) {
|
||||||
|
topLoadLockRef.current = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isNearBottom, loadOlderMessages]);
|
}, [isNearBottom, loadOlderMessages]);
|
||||||
|
|
||||||
@@ -322,6 +341,14 @@ export function useChatSessionState({
|
|||||||
messagesOffsetRef.current = 0;
|
messagesOffsetRef.current = 0;
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
setTotalMessages(0);
|
setTotalMessages(0);
|
||||||
|
setVisibleMessageCount(INITIAL_VISIBLE_MESSAGES);
|
||||||
|
setAllMessagesLoaded(false);
|
||||||
|
allMessagesLoadedRef.current = false;
|
||||||
|
setIsLoadingAllMessages(false);
|
||||||
|
setLoadAllJustFinished(false);
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current);
|
||||||
|
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
||||||
setTokenBudget(null);
|
setTokenBudget(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
@@ -571,6 +598,106 @@ export function useChatSessionState({
|
|||||||
}
|
}
|
||||||
}, [currentSessionId, isLoading, processingSessions, selectedSession?.id]);
|
}, [currentSessionId, isLoading, processingSessions, selectedSession?.id]);
|
||||||
|
|
||||||
|
// Show "Load all" overlay after a batch finishes loading, persist for 2s then hide
|
||||||
|
const prevLoadingRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const wasLoading = prevLoadingRef.current;
|
||||||
|
prevLoadingRef.current = isLoadingMoreMessages;
|
||||||
|
|
||||||
|
if (wasLoading && !isLoadingMoreMessages && hasMoreMessages) {
|
||||||
|
if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current);
|
||||||
|
setShowLoadAllOverlay(true);
|
||||||
|
loadAllOverlayTimerRef.current = setTimeout(() => {
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
if (!hasMoreMessages && !isLoadingMoreMessages) {
|
||||||
|
if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current);
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current);
|
||||||
|
};
|
||||||
|
}, [isLoadingMoreMessages, hasMoreMessages]);
|
||||||
|
|
||||||
|
const loadAllMessages = useCallback(async () => {
|
||||||
|
if (!selectedSession || !selectedProject) return;
|
||||||
|
if (isLoadingAllMessages) return;
|
||||||
|
const sessionProvider = selectedSession.__provider || 'claude';
|
||||||
|
if (sessionProvider === 'cursor') {
|
||||||
|
setVisibleMessageCount(Infinity);
|
||||||
|
setAllMessagesLoaded(true);
|
||||||
|
allMessagesLoadedRef.current = true;
|
||||||
|
setLoadAllJustFinished(true);
|
||||||
|
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
||||||
|
loadAllFinishedTimerRef.current = setTimeout(() => {
|
||||||
|
setLoadAllJustFinished(false);
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestSessionId = selectedSession.id;
|
||||||
|
|
||||||
|
allMessagesLoadedRef.current = true;
|
||||||
|
isLoadingMoreRef.current = true;
|
||||||
|
setIsLoadingAllMessages(true);
|
||||||
|
setShowLoadAllOverlay(true);
|
||||||
|
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
const previousScrollHeight = container ? container.scrollHeight : 0;
|
||||||
|
const previousScrollTop = container ? container.scrollTop : 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await (api.sessionMessages as any)(
|
||||||
|
selectedProject.name,
|
||||||
|
requestSessionId,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
sessionProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentSessionId !== requestSessionId) return;
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const allMessages = data.messages || data;
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
pendingScrollRestoreRef.current = {
|
||||||
|
height: previousScrollHeight,
|
||||||
|
top: previousScrollTop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionMessages(Array.isArray(allMessages) ? allMessages : []);
|
||||||
|
setHasMoreMessages(false);
|
||||||
|
setTotalMessages(Array.isArray(allMessages) ? allMessages.length : 0);
|
||||||
|
messagesOffsetRef.current = Array.isArray(allMessages) ? allMessages.length : 0;
|
||||||
|
|
||||||
|
setVisibleMessageCount(Infinity);
|
||||||
|
setAllMessagesLoaded(true);
|
||||||
|
|
||||||
|
setLoadAllJustFinished(true);
|
||||||
|
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
||||||
|
loadAllFinishedTimerRef.current = setTimeout(() => {
|
||||||
|
setLoadAllJustFinished(false);
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
allMessagesLoadedRef.current = false;
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading all messages:', error);
|
||||||
|
allMessagesLoadedRef.current = false;
|
||||||
|
setShowLoadAllOverlay(false);
|
||||||
|
} finally {
|
||||||
|
isLoadingMoreRef.current = false;
|
||||||
|
setIsLoadingAllMessages(false);
|
||||||
|
}
|
||||||
|
}, [selectedSession, selectedProject, isLoadingAllMessages, currentSessionId]);
|
||||||
|
|
||||||
const loadEarlierMessages = useCallback(() => {
|
const loadEarlierMessages = useCallback(() => {
|
||||||
setVisibleMessageCount((previousCount) => previousCount + 100);
|
setVisibleMessageCount((previousCount) => previousCount + 100);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -599,11 +726,17 @@ export function useChatSessionState({
|
|||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
|
loadAllMessages,
|
||||||
|
allMessagesLoaded,
|
||||||
|
isLoadingAllMessages,
|
||||||
|
loadAllJustFinished,
|
||||||
|
showLoadAllOverlay,
|
||||||
claudeStatus,
|
claudeStatus,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
createDiff,
|
createDiff,
|
||||||
scrollContainerRef,
|
scrollContainerRef,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
|
scrollToBottomAndReset,
|
||||||
isNearBottom,
|
isNearBottom,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
loadSessionMessages,
|
loadSessionMessages,
|
||||||
|
|||||||
@@ -96,11 +96,17 @@ function ChatInterface({
|
|||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
|
loadAllMessages,
|
||||||
|
allMessagesLoaded,
|
||||||
|
isLoadingAllMessages,
|
||||||
|
loadAllJustFinished,
|
||||||
|
showLoadAllOverlay,
|
||||||
claudeStatus,
|
claudeStatus,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
createDiff,
|
createDiff,
|
||||||
scrollContainerRef,
|
scrollContainerRef,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
|
scrollToBottomAndReset,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
} = useChatSessionState({
|
} = useChatSessionState({
|
||||||
selectedProject,
|
selectedProject,
|
||||||
@@ -297,6 +303,11 @@ function ChatInterface({
|
|||||||
visibleMessageCount={visibleMessageCount}
|
visibleMessageCount={visibleMessageCount}
|
||||||
visibleMessages={visibleMessages}
|
visibleMessages={visibleMessages}
|
||||||
loadEarlierMessages={loadEarlierMessages}
|
loadEarlierMessages={loadEarlierMessages}
|
||||||
|
loadAllMessages={loadAllMessages}
|
||||||
|
allMessagesLoaded={allMessagesLoaded}
|
||||||
|
isLoadingAllMessages={isLoadingAllMessages}
|
||||||
|
loadAllJustFinished={loadAllJustFinished}
|
||||||
|
showLoadAllOverlay={showLoadAllOverlay}
|
||||||
createDiff={createDiff}
|
createDiff={createDiff}
|
||||||
onFileOpen={onFileOpen}
|
onFileOpen={onFileOpen}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
@@ -327,7 +338,7 @@ function ChatInterface({
|
|||||||
onClearInput={handleClearInput}
|
onClearInput={handleClearInput}
|
||||||
isUserScrolledUp={isUserScrolledUp}
|
isUserScrolledUp={isUserScrolledUp}
|
||||||
hasMessages={chatMessages.length > 0}
|
hasMessages={chatMessages.length > 0}
|
||||||
onScrollToBottom={scrollToBottom}
|
onScrollToBottom={scrollToBottomAndReset}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isDragActive={isDragActive}
|
isDragActive={isDragActive}
|
||||||
attachedImages={attachedImages}
|
attachedImages={attachedImages}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ interface ChatMessagesPaneProps {
|
|||||||
visibleMessageCount: number;
|
visibleMessageCount: number;
|
||||||
visibleMessages: ChatMessage[];
|
visibleMessages: ChatMessage[];
|
||||||
loadEarlierMessages: () => void;
|
loadEarlierMessages: () => void;
|
||||||
|
loadAllMessages: () => void;
|
||||||
|
allMessagesLoaded: boolean;
|
||||||
|
isLoadingAllMessages: boolean;
|
||||||
|
loadAllJustFinished: boolean;
|
||||||
|
showLoadAllOverlay: boolean;
|
||||||
createDiff: any;
|
createDiff: any;
|
||||||
onFileOpen?: (filePath: string, diffInfo?: unknown) => void;
|
onFileOpen?: (filePath: string, diffInfo?: unknown) => void;
|
||||||
onShowSettings?: () => void;
|
onShowSettings?: () => void;
|
||||||
@@ -76,6 +81,11 @@ export default function ChatMessagesPane({
|
|||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
|
loadAllMessages,
|
||||||
|
allMessagesLoaded,
|
||||||
|
isLoadingAllMessages,
|
||||||
|
loadAllJustFinished,
|
||||||
|
showLoadAllOverlay,
|
||||||
createDiff,
|
createDiff,
|
||||||
onFileOpen,
|
onFileOpen,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
@@ -149,7 +159,8 @@ export default function ChatMessagesPane({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isLoadingMoreMessages && (
|
{/* Loading indicator for older messages (hide when load-all is active) */}
|
||||||
|
{isLoadingMoreMessages && !isLoadingAllMessages && !allMessagesLoaded && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-3">
|
<div className="text-center text-gray-500 dark:text-gray-400 py-3">
|
||||||
<div className="flex items-center justify-center space-x-2">
|
<div className="flex items-center justify-center space-x-2">
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400" />
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400" />
|
||||||
@@ -158,23 +169,69 @@ export default function ChatMessagesPane({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasMoreMessages && !isLoadingMoreMessages && (
|
{/* Indicator showing there are more messages to load (hide when all loaded) */}
|
||||||
|
{hasMoreMessages && !isLoadingMoreMessages && !allMessagesLoaded && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
{totalMessages > 0 && (
|
{totalMessages > 0 && (
|
||||||
<span>
|
<span>
|
||||||
{t('session.messages.showingOf', { shown: sessionMessagesCount, total: totalMessages })} |
|
{t('session.messages.showingOf', { shown: sessionMessagesCount, total: totalMessages })}{' '}
|
||||||
<span className="text-xs">{t('session.messages.scrollToLoad')}</span>
|
<span className="text-xs">{t('session.messages.scrollToLoad')}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Floating "Load all messages" overlay */}
|
||||||
|
{(showLoadAllOverlay || isLoadingAllMessages || loadAllJustFinished) && (
|
||||||
|
<div className="sticky top-2 z-20 flex justify-center pointer-events-none">
|
||||||
|
{loadAllJustFinished ? (
|
||||||
|
<div className="px-4 py-1.5 text-xs font-medium text-white bg-green-600 dark:bg-green-500 rounded-full shadow-lg flex items-center space-x-2">
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<span>{t('session.messages.allLoaded')}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="pointer-events-auto px-4 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded-full shadow-lg transition-all duration-200 hover:scale-105 disabled:opacity-75 disabled:cursor-wait flex items-center space-x-2"
|
||||||
|
onClick={loadAllMessages}
|
||||||
|
disabled={isLoadingAllMessages}
|
||||||
|
>
|
||||||
|
{isLoadingAllMessages && (
|
||||||
|
<div className="animate-spin rounded-full h-3 w-3 border-2 border-white/30 border-t-white" />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{isLoadingAllMessages
|
||||||
|
? t('session.messages.loadingAll')
|
||||||
|
: <>{t('session.messages.loadAll')} {totalMessages > 0 && `(${totalMessages})`}</>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Performance warning when all messages are loaded */}
|
||||||
|
{allMessagesLoaded && (
|
||||||
|
<div className="text-center text-amber-600 dark:text-amber-400 text-xs py-1.5 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
||||||
|
{t('session.messages.perfWarning')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Legacy message count indicator (for non-paginated view) */}
|
||||||
{!hasMoreMessages && chatMessages.length > visibleMessageCount && (
|
{!hasMoreMessages && chatMessages.length > visibleMessageCount && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
{t('session.messages.showingLast', { count: visibleMessageCount, total: chatMessages.length })} |
|
{t('session.messages.showingLast', { count: visibleMessageCount, total: chatMessages.length })} |
|
||||||
<button className="ml-1 text-blue-600 hover:text-blue-700 underline" onClick={loadEarlierMessages}>
|
<button className="ml-1 text-blue-600 hover:text-blue-700 underline" onClick={loadEarlierMessages}>
|
||||||
{t('session.messages.loadEarlier')}
|
{t('session.messages.loadEarlier')}
|
||||||
</button>
|
</button>
|
||||||
|
{' | '}
|
||||||
|
<button
|
||||||
|
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline"
|
||||||
|
onClick={loadAllMessages}
|
||||||
|
>
|
||||||
|
{t('session.messages.loadAll')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,11 @@
|
|||||||
"showingOf": "Showing {{shown}} of {{total}} messages",
|
"showingOf": "Showing {{shown}} of {{total}} messages",
|
||||||
"scrollToLoad": "Scroll up to load more",
|
"scrollToLoad": "Scroll up to load more",
|
||||||
"showingLast": "Showing last {{count}} messages ({{total}} total)",
|
"showingLast": "Showing last {{count}} messages ({{total}} total)",
|
||||||
"loadEarlier": "Load earlier messages"
|
"loadEarlier": "Load earlier messages",
|
||||||
|
"loadAll": "Load all messages",
|
||||||
|
"loadingAll": "Loading all messages...",
|
||||||
|
"allLoaded": "All messages loaded",
|
||||||
|
"perfWarning": "All messages loaded — scrolling may be slower. Click \"Scroll to bottom\" to restore performance."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|||||||
@@ -175,7 +175,11 @@
|
|||||||
"showingOf": "{{total}}개 중 {{shown}}개 표시",
|
"showingOf": "{{total}}개 중 {{shown}}개 표시",
|
||||||
"scrollToLoad": "위로 스크롤하여 더 로드",
|
"scrollToLoad": "위로 스크롤하여 더 로드",
|
||||||
"showingLast": "마지막 {{count}}개 메시지 표시 (총 {{total}}개)",
|
"showingLast": "마지막 {{count}}개 메시지 표시 (총 {{total}}개)",
|
||||||
"loadEarlier": "이전 메시지 로드"
|
"loadEarlier": "이전 메시지 로드",
|
||||||
|
"loadAll": "모든 메시지 로드",
|
||||||
|
"loadingAll": "모든 메시지 로딩 중...",
|
||||||
|
"allLoaded": "모든 메시지 로드 완료",
|
||||||
|
"perfWarning": "모든 메시지가 로드됨 - 스크롤이 느려질 수 있습니다. \"맨 아래로 스크롤\"을 클릭하면 성능이 복구됩니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|||||||
@@ -175,7 +175,11 @@
|
|||||||
"showingOf": "显示 {{shown}} / {{total}} 条消息",
|
"showingOf": "显示 {{shown}} / {{total}} 条消息",
|
||||||
"scrollToLoad": "向上滚动以加载更多",
|
"scrollToLoad": "向上滚动以加载更多",
|
||||||
"showingLast": "显示最近 {{count}} 条消息(共 {{total}} 条)",
|
"showingLast": "显示最近 {{count}} 条消息(共 {{total}} 条)",
|
||||||
"loadEarlier": "加载更早的消息"
|
"loadEarlier": "加载更早的消息",
|
||||||
|
"loadAll": "加载全部消息",
|
||||||
|
"loadingAll": "正在加载全部消息...",
|
||||||
|
"allLoaded": "全部消息已加载",
|
||||||
|
"perfWarning": "已加载全部消息 - 滚动可能变慢。点击「滚动到底部」恢复性能。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|||||||
Reference in New Issue
Block a user