feat(chat): implement infinite scroll for message loading and optimize chat message rendering

- Added infinite scroll functionality to load older messages when the user scrolls near the top of the chat.
- Implemented pagination state management to track loading status and available messages.
- Enhanced ChatMessage component with React.memo for performance optimization during streaming.
- Updated database interactions to support paginated message retrieval, improving efficiency in loading chat history.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-27 21:53:27 -03:00
1 parent e53e49c8f8
commit 0d19f496bb
5 files changed
+761 -18

No files matched your search

+157 -8
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Message } from './use-chat';
import { removeMetadataNoise } from '@/lib/metadata';
@@ -13,6 +13,28 @@ export interface SessionSummary {
match_count?: number;
}
/** Resultado de carregamento paginado do backend */
interface PaginatedHistoryResult {
messages: Array<{
role: string;
content: string;
metadata?: unknown;
}>;
total_count: number;
has_more: boolean;
}
/** Estado de paginação para uma sessão */
export interface PaginationState {
totalCount: number;
hasMore: boolean;
offset: number;
isLoadingMore: boolean;
}
/** Número de mensagens a carregar por página */
const MESSAGES_PER_PAGE = 30;
const TITLE_MODEL = "qwen2.5:0.5b";
// Helper to generate fallback title from user's first message
@@ -41,6 +63,17 @@ export function useChatStorage() {
const [isGeneratingTitle, setIsGeneratingTitle] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>('');
const [isSearching, setIsSearching] = useState(false);
// Estado de paginação por sessão
const [paginationState, setPaginationState] = useState<PaginationState>({
totalCount: 0,
hasMore: false,
offset: 0,
isLoadingMore: false,
});
// Ref para evitar múltiplos loads simultâneos
const loadingMoreRef = useRef(false);
const loadSessions = useCallback(async () => {
try {
@@ -74,23 +107,138 @@ export function useChatStorage() {
}
}, [loadSessions]);
const loadSessionHistory = useCallback(async (id: string): Promise<Message[]> => {
/**
* Carrega histórico de uma sessão com paginação (lazy loading)
*
* Por padrão, carrega as últimas MESSAGES_PER_PAGE mensagens.
* Retorna as mensagens e atualiza o estado de paginação.
*/
const loadSessionHistory = useCallback(async (
id: string,
options?: { loadAll?: boolean }
): Promise<Message[]> => {
try {
const rawMessages = await invoke<any[]>('load_chat_history', { id });
// Convert role from String to type-safe union and ensure proper types
return rawMessages.map(msg => ({
// Reset pagination state
setPaginationState({
totalCount: 0,
hasMore: false,
offset: 0,
isLoadingMore: false,
});
// Se loadAll = true, usa o comando antigo que carrega tudo
if (options?.loadAll) {
const rawMessages = await invoke<Array<{
role: string;
content: string;
metadata?: unknown;
}>>('load_chat_history', { id });
return rawMessages.map(msg => ({
role: (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system')
? msg.role as 'user' | 'assistant' | 'system'
: 'user' as const,
content: msg.content || '',
metadata: msg.metadata || undefined
}));
}
// Usar paginação: carregar últimas N mensagens
const result = await invoke<PaginatedHistoryResult>('load_chat_history_paginated', {
id,
limit: MESSAGES_PER_PAGE,
offset: 0
});
// Atualizar estado de paginação
setPaginationState({
totalCount: result.total_count,
hasMore: result.has_more,
offset: result.messages.length,
isLoadingMore: false,
});
// Converter para tipo Message
return result.messages.map(msg => ({
role: (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system')
? msg.role as 'user' | 'assistant' | 'system'
: 'user' as const, // fallback
: 'user' as const,
content: msg.content || '',
metadata: msg.metadata || undefined
}));
} catch (error) {
console.error("Failed to load history:", error);
return [];
// Fallback: tentar carregar tudo com o comando antigo
try {
const rawMessages = await invoke<Array<{
role: string;
content: string;
metadata?: unknown;
}>>('load_chat_history', { id });
return rawMessages.map(msg => ({
role: (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system')
? msg.role as 'user' | 'assistant' | 'system'
: 'user' as const,
content: msg.content || '',
metadata: msg.metadata || undefined
}));
} catch {
return [];
}
}
}, []);
/**
* Carrega mais mensagens antigas (scroll infinito reverso)
*
* Chamado quando o usuário faz scroll para o topo do chat.
* Retorna as mensagens anteriores que devem ser prepend ao estado.
*/
const loadMoreMessages = useCallback(async (
sessionId: string
): Promise<Message[]> => {
// Evitar múltiplos loads simultâneos
if (loadingMoreRef.current || !paginationState.hasMore) {
return [];
}
loadingMoreRef.current = true;
setPaginationState(prev => ({ ...prev, isLoadingMore: true }));
try {
const result = await invoke<PaginatedHistoryResult>('load_chat_history_paginated', {
id: sessionId,
limit: MESSAGES_PER_PAGE,
offset: paginationState.offset
});
// Atualizar estado de paginação
setPaginationState(prev => ({
...prev,
hasMore: result.has_more,
offset: prev.offset + result.messages.length,
isLoadingMore: false,
}));
// Converter para tipo Message
return result.messages.map(msg => ({
role: (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system')
? msg.role as 'user' | 'assistant' | 'system'
: 'user' as const,
content: msg.content || '',
metadata: msg.metadata || undefined
}));
} catch (error) {
console.error("Failed to load more messages:", error);
setPaginationState(prev => ({ ...prev, isLoadingMore: false }));
return [];
} finally {
loadingMoreRef.current = false;
}
}, [paginationState.hasMore, paginationState.offset]);
const deleteSession = useCallback(async (id: string) => {
try {
await invoke('delete_chat_session', { id });
@@ -397,6 +545,8 @@ export function useChatStorage() {
setCurrentSessionId,
loadSessions,
loadSessionHistory,
loadMoreMessages,
paginationState,
saveSession,
deleteSession,
isGeneratingTitle,
@@ -408,4 +558,3 @@ export function useChatStorage() {
isSearching
};
}