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 0358a6ea2d
commit 6cc3f71f38
7 files changed
+395 -116

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ export interface SessionSummary {
updated_at: string;
preview: string;
platform: string;
match_count?: number;
}
const TITLE_MODEL = "qwen2.5:0.5b";
+40 -26
View File
@@ -240,6 +240,29 @@ export function useChat() {
const rawContentRef = useRef<string>(''); // Track raw content including metadata during streaming
const currentSessionIdRef = useRef<string | null>(null);
const lastTokenRef = useRef<string>(''); // Track last token to detect duplicates
const renderScheduledRef = useRef<boolean>(false);
const rafIdRef = useRef<number | null>(null);
// Função para atualizar o estado visual com o conteúdo acumulado
const flushToState = () => {
renderScheduledRef.current = false;
const displayContent = stripMetadataTag(rawContentRef.current);
setMessages(prev => {
const last = prev[prev.length - 1];
if (last && last.role === 'assistant') {
return [...prev.slice(0, -1), { ...last, content: displayContent }];
}
return [...prev, { role: 'assistant', content: displayContent }];
});
};
// Agendar flush com requestAnimationFrame (limita a ~60fps)
const scheduleFlush = () => {
if (!renderScheduledRef.current) {
renderScheduledRef.current = true;
rafIdRef.current = requestAnimationFrame(flushToState);
}
};
// Listener para eventos do Rust
useEffect(() => {
@@ -262,51 +285,38 @@ export function useChat() {
const { content, done } = event.payload;
if (content && content.length > 0) {
// Verificar se este é o mesmo token do último processado (evitar duplicação)
// Isso pode acontecer se o evento for emitido múltiplas vezes
// Detecção de duplicatas (MANTER LÓGICA ORIGINAL)
if (content === lastTokenRef.current && rawContentRef.current.endsWith(content)) {
// Token duplicado, ignorar
if (process.env.NODE_ENV === 'development') {
console.warn('Token duplicado detectado, ignorando:', content.substring(0, 30));
}
return;
}
// Atualizar referência do último token
lastTokenRef.current = content;
// Accumulate raw content (including metadata) in ref
rawContentRef.current += content;
setMessages(prev => {
const last = prev[prev.length - 1];
if (last && last.role === 'assistant') {
// For display: strip incomplete metadata tag if present
const displayContent = stripMetadataTag(rawContentRef.current);
return [
...prev.slice(0, -1),
{ ...last, content: displayContent }
];
}
// Se não existe mensagem do assistente, criar uma
return [...prev, { role: 'assistant', content: stripMetadataTag(rawContentRef.current) }];
});
// MUDANÇA: Agendar flush em vez de chamar setMessages diretamente
scheduleFlush();
}
if (done) {
// Stream terminou, processar metadata final
// Cancelar qualquer flush pendente
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
renderScheduledRef.current = false;
}
// Flush IMEDIATO com processamento de metadata
setMessages(prev => {
const last = prev[prev.length - 1];
if (last && last.role === 'assistant') {
const rawContent = rawContentRef.current;
const { content: finalContent, metadata } = parseMetadata(rawContent);
rawContentRef.current = '';
lastTokenRef.current = ''; // Reset last token tracker
return [
...prev.slice(0, -1),
{ ...last, content: finalContent || last.content, metadata }
];
lastTokenRef.current = '';
return [...prev.slice(0, -1), { ...last, content: finalContent || last.content, metadata }];
}
return prev;
});
@@ -331,6 +341,10 @@ export function useChat() {
unlistenCreated?.();
unlistenToken?.();
unlistenError?.();
// Cancelar RAF pendente
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, []);