feat(chat): enhance search functionality and message highlighting

- Added search match navigation to allow users to cycle through search results in chat sessions.
- Implemented highlighting of search terms in chat messages for better visibility.
- Updated sidebar to display match counts for sessions based on search results.
- Enhanced session search logic in the database to include match counts for improved user experience.
- Refactored chat message component to support dynamic highlighting of search terms.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-27 20:39:39 -03:00
1 parent 1a64f5aa18
commit ba03131a78
7 files changed
+395 -116

No files matched your search

+105 -2
View File
@@ -52,7 +52,8 @@ export default function ChatPage() {
deleteSession,
isGeneratingTitle,
searchSessions,
isSearching
isSearching,
searchQuery
} = useChatStorage();
const { isDownloading, progress } = useAutoLabelingModel();
@@ -128,6 +129,14 @@ export default function ChatPage() {
const [showDownloadDialog, setShowDownloadDialog] = useState(false);
const [logsCopied, setLogsCopied] = useState(false);
const [lastUserQuery, setLastUserQuery] = useState('');
const [searchMatchIndex, setSearchMatchIndex] = useState(0);
// Resetar índice quando busca é limpa ou sessão muda
useEffect(() => {
if (!searchQuery || searchQuery.trim().length < 2) {
setSearchMatchIndex(0);
}
}, [searchQuery, currentSessionId]);
// Auto-select first model
useEffect(() => {
@@ -803,7 +812,61 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
setMessages(history);
setCurrentSessionId(id);
lastSavedMessagesRef.current = history.length;
// Se há busca ativa, encontrar matches e resetar índice
if (searchQuery && searchQuery.trim().length >= 2) {
setSearchMatchIndex(0);
// Os matches serão encontrados quando as mensagens renderizarem
}
};
// Função para navegar entre matches
const handleNavigateMatch = useCallback((sessionId: string, direction: 'prev' | 'next') => {
if (sessionId !== currentSessionId || !searchQuery || searchQuery.trim().length < 2) {
return;
}
// Encontrar todos os matches nas mensagens
const query = searchQuery.trim();
const matches: Array<{ messageIndex: number; matchIndex: number }> = [];
messages.forEach((msg, msgIdx) => {
if (msg.role === 'user' || msg.role === 'assistant') {
const content = msg.content;
const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
let match;
let matchIdx = 0;
while ((match = regex.exec(content)) !== null) {
matches.push({ messageIndex: msgIdx, matchIndex: matchIdx });
matchIdx++;
}
}
});
if (matches.length === 0) return;
// Navegar
setSearchMatchIndex(prev => {
const newIndex = direction === 'next'
? (prev + 1) % matches.length
: (prev - 1 + matches.length) % matches.length;
// Scroll para o match após renderização
setTimeout(() => {
const messageElement = document.querySelector(`[data-message-index="${matches[newIndex].messageIndex}"]`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Destacar o match atual visualmente
const marks = messageElement.querySelectorAll('mark');
if (marks.length > matches[newIndex].matchIndex) {
marks[matches[newIndex].matchIndex]?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
}
}
}, 150);
return newIndex;
});
}, [currentSessionId, searchQuery, messages]);
const handleNewChat = () => {
stop();
@@ -947,6 +1010,8 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
onNewChat={handleNewChat}
onSearch={searchSessions}
isSearching={isSearching}
searchQuery={searchQuery}
onNavigateMatch={handleNavigateMatch}
/>
</ResizablePanel>
@@ -1132,6 +1197,25 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
});
}
// Calcular posições de todos os matches uma vez
const allMatches: Array<{ messageIndex: number; matchIndex: number }> = [];
if (searchQuery && searchQuery.trim().length >= 2) {
const query = searchQuery.trim();
const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
messages.forEach((msg, msgIdx) => {
if (msg.role === 'user' || msg.role === 'assistant') {
const content = msg.content;
let match;
let matchIdx = 0;
while ((match = regex.exec(content)) !== null) {
allMatches.push({ messageIndex: msgIdx, matchIndex: matchIdx });
matchIdx++;
}
}
});
}
return groupedMessages.map((group, idx) => {
if (group.type === 'thinking-group' && group.thinkingSteps) {
return (
@@ -1143,11 +1227,30 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
);
} else if (group.type === 'regular' && group.message) {
// Encontrar índice do match atual para esta mensagem
let messageMatchIndex: number | undefined = undefined;
if (searchQuery && searchQuery.trim().length >= 2 && allMatches.length > 0 && group.index !== undefined) {
// Encontrar matches desta mensagem
const messageMatches = allMatches.filter(m => m.messageIndex === group.index);
if (messageMatches.length > 0) {
// Verificar se o match atual está nesta mensagem
const currentMatch = allMatches[searchMatchIndex];
if (currentMatch && currentMatch.messageIndex === group.index) {
messageMatchIndex = currentMatch.matchIndex;
}
}
}
return (
<div key={group.index || idx} className="animate-in fade-in slide-in-from-bottom-4 duration-500">
<ChatMessage
message={group.message}
isStreaming={group.isStreaming}
isStreaming={group.isStreaming}
highlightTerm={searchQuery && searchQuery.trim().length >= 2 ? searchQuery.trim() : undefined}
highlightIndex={messageMatchIndex}
messageIndex={group.index}
/>
</div>
);