'use client'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, RefreshCw, Cpu } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { ModelSelector } from "@/components/chat/model-selector"; import { ChatInput } from "@/components/chat/chat-input"; import { ChatMessage } from "@/components/chat/chat-message"; import { ReasoningChain } from "@/components/chat/reasoning-chain"; import { TitleBar } from "@/components/titlebar"; import { SidebarList } from "@/components/chat/sidebar-list"; import { useChat } from "@/hooks/use-chat"; import { useLocalModels } from "@/hooks/use-local-models"; import { useModelSelection } from "@/hooks/use-model-selection"; import { useChatStorage } from "@/hooks/use-chat-storage"; import { useAutoLabelingModel } from "@/hooks/use-auto-labeling-model"; import { useWebSearch } from "@/hooks/use-web-search"; // SearchProgress removido - informações agora nas mensagens de processo // ThinkingIndicator removido - usando mensagens de processo integradas // Componentes de processamento removidos - agora usando mensagens de processo integradas na timeline import { useQueryGenerator } from "@/hooks/use-query-generator"; import { useDeepResearch } from "@/hooks/use-deep-research"; import { DEEP_RESEARCH_PROMPTS } from "@/data/prompts/deep-research"; import { useSettingsStore } from "@/store/settings-store"; import type { ScrapedContent } from "@/services/webSearch"; import { useState, useEffect, useRef, useCallback, startTransition } from "react"; import { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; import { invoke } from "@tauri-apps/api/core"; import { ImperativePanelHandle } from "react-resizable-panels"; import { ModelDownloadDialog } from "@/components/chat/model-download-dialog"; import { chatLog } from "@/lib/terminal-logger"; import { useQueryPreprocessor, type PreprocessedQuery } from "@/hooks/use-query-preprocessor"; import type { Message, ThinkingMessageMetadata, ThinkingStepType, ThinkingStepStatus } from "@/hooks/use-chat"; import { executeProgressiveSearch } from "@/lib/web-search-fallback"; import type { DebugData } from "@/components/chat/debug-console"; // @ts-expect-error - MD file import import defaultFormatPrompt from "@/data/prompts/default-format.md"; export default function ChatPage() { const router = useRouter(); const { messages, setMessages, sendMessage, isLoading, stop, clearChat } = useChat(); const { models, refresh } = useLocalModels(); const { theme, setTheme } = useTheme(); const { isSetupCompleted } = useSettingsStore(); const { sessions, currentSessionId, setCurrentSessionId, loadSessionHistory, loadMoreMessages, paginationState, saveSession, deleteSession, isGeneratingTitle, searchSessions, isSearching, searchQuery } = useChatStorage(); const { isDownloading, progress } = useAutoLabelingModel(); const webSearch = useWebSearch(); const { generateQuery } = useQueryGenerator(); const deepResearch = useDeepResearch(); const settings = useSettingsStore(); const { preprocess } = useQueryPreprocessor(); // Funções auxiliares para gerenciar mensagens de processo const addThinkingMessage = useCallback(( stepType: ThinkingStepType, label: string, status: ThinkingStepStatus = 'running', details?: string, progress?: number ): string => { const messageId = `thinking-${stepType}-${Date.now()}`; const thinkingMessage: Message = { role: 'system', content: '', metadata: { type: 'thinking', stepType, status, label, details, progress, timestamp: Date.now(), } as ThinkingMessageMetadata, }; setMessages(prev => [...prev, thinkingMessage]); return messageId; }, [setMessages]); const updateThinkingMessage = useCallback(( stepType: ThinkingStepType, updates: Partial ) => { setMessages(prev => prev.map(msg => { const metadata = msg.metadata as ThinkingMessageMetadata | undefined; if (metadata?.type === 'thinking' && metadata.stepType === stepType) { return { ...msg, metadata: { ...metadata, ...updates, } as ThinkingMessageMetadata, }; } return msg; })); }, [setMessages]); const { selectedModel, setSelectedModel } = useModelSelection(); // Sincronizar eventos de busca com thinking messages useEffect(() => { if (webSearch.activeQueries.length > 0 || webSearch.activeUrls.length > 0) { updateThinkingMessage('web-research', { activeQueries: webSearch.activeQueries, activeUrls: webSearch.activeUrls, }); } }, [webSearch.activeQueries, webSearch.activeUrls, updateThinkingMessage]); const [mounted, setMounted] = useState(false); const initializedRef = useRef(false); // Refs para evitar loops e execuções múltiplas const hasCheckedIntegrityRef = useRef(false); const isRedirectingRef = useRef(false); // Guarda de roteamento: verificar se setup foi completado useEffect(() => { if (!isSetupCompleted && !isRedirectingRef.current) { isRedirectingRef.current = true; startTransition(() => { router.replace("/setup"); // Mudar de push para replace para consistência }); } }, [isSetupCompleted, router]); // Guarda de integridade: verificar se Ollama está funcional // Executa SEMPRE que o componente montar, independente do status do setup useEffect(() => { if (hasCheckedIntegrityRef.current) return; // Proteção contra múltiplas execuções if (isRedirectingRef.current) return; // Evitar redirecionamentos múltiplos const checkIntegrity = async () => { try { const isOllamaIntact = await invoke('verify_ollama_integrity_command'); if (!isOllamaIntact && !isRedirectingRef.current) { isRedirectingRef.current = true; // Ollama não está funcional - redirecionar para setup console.warn("Integridade do Ollama falhou no chat. Redirecionando para Setup."); startTransition(() => { router.replace('/setup'); }); } } catch (e) { // Em caso de erro na verificação, redirecionar para setup (fallback seguro) if (!isRedirectingRef.current) { isRedirectingRef.current = true; console.error("Falha ao verificar integridade do Ollama no chat:", e); startTransition(() => { router.replace('/setup'); }); } } finally { hasCheckedIntegrityRef.current = true; // Marcar como executado } }; checkIntegrity(); }, [router]); // Remover isSetupCompleted das dependências - executar sempre // Initialize with default format prompt const [systemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo."); const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(true); // Iniciar colapsada // Removido: thinkingStep e processSteps - agora usando mensagens thinking agrupadas const [, setError] = useState(null); const chatsSidebarRef = useRef(null); const saveTimeoutRef = useRef(null); const lastSavedMessagesRef = useRef(0); const messagesEndRef = useRef(null); const scrollContainerRef = useRef(null); const [showDownloadDialog, setShowDownloadDialog] = useState(false); const [searchMatchIndex, setSearchMatchIndex] = useState(0); const [debugDataMap, setDebugDataMap] = useState>(new Map()); // Resetar índice quando busca é limpa ou sessão muda useEffect(() => { if (!searchQuery || searchQuery.trim().length < 2) { setSearchMatchIndex(0); } }, [searchQuery, currentSessionId]); useEffect(() => { setMounted(true); }, []); // Auto-scroll para o final durante streaming useEffect(() => { if (messagesEndRef.current && (isLoading || messages.length > 0)) { // Pequeno delay para garantir que o DOM foi atualizado setTimeout(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, 100); } }, [messages, isLoading]); // Save session when loading finishes (with debounce to avoid race conditions) useEffect(() => { // Clear any pending save if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } // Only save if: // 1. Not currently loading // 2. We have a session ID // 3. We have messages // 4. Messages actually changed (by count or reference) if (!isLoading && currentSessionId && messages.length > 0) { // Check if messages actually changed (simple length check, could be improved) const currentMessageCount = messages.length; if (currentMessageCount !== lastSavedMessagesRef.current) { // Small delay to ensure metadata processing is complete saveTimeoutRef.current = setTimeout(() => { lastSavedMessagesRef.current = currentMessageCount; saveSession(currentSessionId, messages); }, 100); } } // Cleanup timeout on unmount or dependency change return () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } }; }, [isLoading, currentSessionId, messages, saveSession]); // Função auxiliar para pré-processar query (memoizada com useCallback) const preprocessQuery = useCallback(async (content: string): Promise<{ preprocessed: PreprocessedQuery; finalContent: string; }> => { const preprocessingConfig = settings.queryPreprocessing || { enabled: true, minLength: 3, maxLength: 2000, autoSplitQuestions: true, irrelevantPatterns: [], }; let preprocessed: PreprocessedQuery; if (preprocessingConfig.enabled) { chatLog.info('\n========== PRE-PROCESSING =========='); preprocessed = await preprocess(content); chatLog.info(`Original: "${preprocessed.original}"`); chatLog.info(`Normalized: "${preprocessed.normalized}"`); chatLog.info(`Intent: ${preprocessed.intent}`); chatLog.info(`Should Search: ${preprocessed.shouldSearch}`); chatLog.info(`Questions Split: ${preprocessed.splitResult.splitCount}`); // Se inválida, retornar erro if (!preprocessed.validation.isValid) { chatLog.error(`Query validation failed: ${preprocessed.validation.errors.join(', ')}`); throw new Error(`Query validation failed: ${preprocessed.validation.errors.join(', ')}`); } // Se múltiplas perguntas detectadas, usar apenas a primeira (com aviso) let finalContent = preprocessed.original; if (preprocessed.questions.length > 1) { chatLog.warn(`Multiple questions detected (${preprocessed.questions.length}). Processing first question only.`); finalContent = preprocessed.questions[0]; } return { preprocessed, finalContent }; } else { // Se pré-processamento desabilitado, criar objeto básico preprocessed = { original: content, normalized: content, questions: [content], intent: 'unknown' as const, validation: { isValid: true, errors: [], warnings: [], normalizedLength: content.length }, shouldSearch: webSearch.isEnabled, splitResult: { questions: [content], originalText: content, splitCount: 1 }, }; return { preprocessed, finalContent: content }; } }, [settings.queryPreprocessing, webSearch.isEnabled, preprocess]); // ========== EXECUÇÃO ESPECULATIVA DE BUSCA ========== // Heurística rápida para detectar se a query provavelmente precisa de busca web const mightNeedWebSearch = useCallback((text: string): boolean => { if (!webSearch.isEnabled) return false; const lowerText = text.toLowerCase(); // Padrões que indicam alta probabilidade de precisar de busca const questionPatterns = /\b(o que|como|quando|onde|quem|porque|por que|qual|quais|quanto|quantos|me explique|me diga|existe|há)\b/i; const recentPatterns = /\b(hoje|ontem|esta semana|este mês|este ano|atualmente|recentemente|últimas notícias|novo|novos|última|últimas|2024|2025)\b/i; const searchPatterns = /\?$|buscar|pesquisar|encontrar|procurar|descobrir/i; // Se contém padrões de pergunta ou busca, provavelmente precisa de pesquisa if (questionPatterns.test(lowerText) || recentPatterns.test(lowerText) || searchPatterns.test(lowerText)) { chatLog.debug('[SpeculativeSearch] Query might need web search (pattern match)'); return true; } // Se tem mais de 5 palavras e termina com ?, provavelmente é uma pergunta const wordCount = text.trim().split(/\s+/).length; if (wordCount > 5 && text.trim().endsWith('?')) { chatLog.debug('[SpeculativeSearch] Query might need web search (question detected)'); return true; } return false; }, [webSearch.isEnabled]); const handleSend = async (content: string) => { if (!selectedModel) return; // ========== ADICIONAR MENSAGENS IMEDIATAMENTE ========== // Adicionar mensagem do usuário ao estado antes de qualquer processamento // para feedback visual instantâneo setMessages(prev => [...prev, { role: 'user', content }]); // Adicionar mensagem de processo: Pré-processamento addThinkingMessage('preprocessing', 'Pré-processamento', 'running'); // Removido: setProcessSteps - agora usando mensagens thinking agrupadas // Limpar erros anteriores setError(null); // ========== EXECUÇÃO ESPECULATIVA ========== // Iniciar busca web especulativamente se a query parecer precisar // Isso reduz latência sobrepondo busca com pré-processamento let speculativeSearchPromise: Promise | null = null; const shouldSpeculate = mightNeedWebSearch(content); if (shouldSpeculate) { chatLog.info('[SpeculativeSearch] Starting speculative web search in parallel with preprocessing'); // Iniciar busca sem await - será usada depois se confirmada speculativeSearchPromise = webSearch.smartSearchRag(content, settings.webSearch?.maxResults || 5) .catch(err => { chatLog.debug(`[SpeculativeSearch] Speculative search failed (will use normal flow): ${err}`); return [] as ScrapedContent[]; }); } // ========== PRÉ-PROCESSAMENTO ========== let preprocessed: PreprocessedQuery; let finalContent: string; const preprocessingStart = Date.now(); try { const result = await preprocessQuery(content); preprocessed = result.preprocessed; finalContent = result.finalContent; content = finalContent; // Atualizar content para uso posterior // Atualizar mensagem de processo: Pré-processamento concluído updateThinkingMessage('preprocessing', { status: 'completed', duration: Date.now() - preprocessingStart, }); } catch (error) { // Se validação falhar, remover mensagens adicionadas e mostrar erro setMessages(prev => { const newMessages = [...prev]; // Remover última mensagem de processo e última do usuário return newMessages.slice(0, -2); }); // Atualizar step com erro const errorMsg = error instanceof Error ? error.message : String(error); updateThinkingMessage('preprocessing', { status: 'error', error: errorMsg, duration: Date.now() - preprocessingStart, }); // Removido: setProcessSteps - agora usando mensagens thinking agrupadas // Definir erro global const errorObj = error instanceof Error ? error : new Error(String(error)); setError(errorObj); chatLog.error(`Preprocessing failed: ${errorObj.message}`); // Erro já será mostrado no chat-input, apenas retornar return; } // ========== TERMINAL LOGGING ========== chatLog.info('\n========== NEW QUERY =========='); chatLog.info(`User Query: "${content}"`); chatLog.info(`Model: ${selectedModel}`); chatLog.info(`Web Search: ${webSearch.isEnabled ? 'ENABLED' : 'DISABLED'}`); chatLog.info(`Intent: ${preprocessed.intent}`); chatLog.info(`Should Search: ${preprocessed.shouldSearch}`); chatLog.info(`Timestamp: ${new Date().toISOString()}`); // Generate ID if new session (but don't create card yet - wait for title) if (!currentSessionId) { const newId = crypto.randomUUID(); setCurrentSessionId(newId); chatLog.info(`New session created: ${newId}`); } // Função auxiliar para executar pesquisa web const executeWebResearch = async ( content: string, preprocessed: PreprocessedQuery, speculativeResults?: ScrapedContent[] | null ): Promise<{ knowledgeBaseContext: string; scrapedSources: ScrapedContent[]; validationReport: string; }> => { let knowledgeBaseContext = ''; let scrapedSources: ScrapedContent[] = []; let validationReport = ''; // Roteamento inteligente: só buscar se shouldSearch for true if (webSearch.isEnabled && preprocessed.shouldSearch) { chatLog.info('\n--- STARTING DEEP RESEARCH PIPELINE ---'); // Se temos resultados especulativos, usá-los como seed inicial if (speculativeResults && speculativeResults.length > 0) { chatLog.info(`[SpeculativeSearch] Using ${speculativeResults.length} speculative results as seed`); deepResearch.addToKnowledgeBase('speculative_seed', speculativeResults); } // Adicionar mensagem de processo: Pesquisa Web addThinkingMessage('web-research', 'Pesquisando na Web...', 'running'); const webResearchStart = Date.now(); try { webSearch.reset(); deepResearch.reset(); // Passo 1: Decomposição chatLog.info('Step 1: Decomposition'); const searchPlan = await deepResearch.decompose(content, selectedModel); chatLog.info(`Search Plan: ${JSON.stringify(searchPlan)}`); if (searchPlan && searchPlan.length > 0) { // Passo 2: Executar buscas com fallback progressivo chatLog.info(`\nStep 2: Executing ${searchPlan.length} searches with progressive fallback`); // Atualizar progresso: decomposição concluída updateThinkingMessage('web-research', { label: `Buscando ${searchPlan.length} consultas...`, progress: 10, details: `Executando busca progressiva com fallback automático...`, }); const maxResults = Math.max(2, Math.floor(settings.webSearch.maxResults / searchPlan.length)); chatLog.info(`Max results per query: ${maxResults}`); // Executar busca progressiva para cada query do plano const searchPromises = searchPlan.map(async (query, idx) => { const queryStartTime = Date.now(); chatLog.info(`\n[WebResearch] ========== QUERY ${idx + 1}/${searchPlan.length} ==========`); chatLog.info(`[WebResearch] Query: "${query}"`); chatLog.info(`[WebResearch] Timestamp: ${new Date().toISOString()}`); // Atualizar progresso updateThinkingMessage('web-research', { details: `Buscando consulta ${idx + 1}/${searchPlan.length}: "${query}"...`, progress: 10 + (idx * 20 / searchPlan.length), }); try { // Obter contexto e queries enriquecidas do deep research const context = deepResearch.state.context; const enrichedQueries = deepResearch.state.enrichedQueries; if (context) { chatLog.info(`[WebResearch] Using contextual analysis:`, { intent: context.intent, entities: context.entities.length, topics: context.topics.length, }); } if (enrichedQueries) { chatLog.info(`[WebResearch] Using enriched queries:`, { literal: enrichedQueries.literal.length, semantic: enrichedQueries.semantic.length, related: enrichedQueries.related.length, }); } // Executar busca progressiva com semantic search se disponível const fallbackResult = await executeProgressiveSearch( query, async (q, limit, round = 1) => { // Passar round para timeout adaptativo return await webSearch.smartSearchRag(q, limit, round); }, selectedModel, { maxRounds: 3, // Máximo 3 rodadas por query maxResultsPerRound: maxResults, maxTotalResults: 30, // Máximo 30 resultados por query minRelevanceScore: 0.3, enableQueryExpansion: !enrichedQueries, // Desabilitar expansão tradicional se usar enriquecimento useSemanticSearch: !!enrichedQueries, // Usar semantic search se temos queries enriquecidas context: context || undefined, enrichedQueries: enrichedQueries || undefined, initialTimeout: settings.webSearch.timeout || 10000, // Usar timeout das configurações maxTimeoutPerQuery: 25000, // 25s máximo por query maxTimeoutPerRound: 15000, // 15s máximo por round } ); const queryDuration = Date.now() - queryStartTime; // Log detalhado do resultado chatLog.info(`\n[WebResearch] ========== QUERY ${idx + 1} RESULTS ==========`); chatLog.info(`[WebResearch] Duration: ${queryDuration}ms`); chatLog.info(`[WebResearch] Success: ${fallbackResult.success}`); chatLog.info(`[WebResearch] Total results: ${fallbackResult.scrapedSources.length}`); chatLog.info(`[WebResearch] Total analyzed: ${fallbackResult.totalResultsAnalyzed}`); chatLog.info(`[WebResearch] Attempts: ${fallbackResult.attempts.length}`); chatLog.info(`[WebResearch] Used fallback: ${fallbackResult.usedFallback}`); // Log detalhado de cada tentativa fallbackResult.attempts.forEach((attempt, attemptIdx) => { chatLog.info(`\n Attempt ${attemptIdx + 1}:`); chatLog.info(` Round: ${attempt.round}`); chatLog.info(` Strategy: ${attempt.strategy || 'N/A'}`); chatLog.info(` Query: "${attempt.query}"`); chatLog.info(` Results: ${attempt.results.length}`); chatLog.info(` Relevance score: ${attempt.relevanceScore.toFixed(3)}`); chatLog.info(` Duration: ${attempt.duration}ms`); }); // Log detalhado de cada resultado extraído if (fallbackResult.scrapedSources.length > 0) { chatLog.info(`\n[WebResearch] Extracted Sources:`); fallbackResult.scrapedSources.forEach((result, resultIdx) => { chatLog.info(`\n [${resultIdx + 1}] ${result.title}`); chatLog.info(` URL: ${result.url}`); const contentLength = result.markdown?.length || result.content?.length || 0; const snippetLength = result.snippet?.length || 0; chatLog.info(` Markdown length: ${contentLength} chars`); chatLog.info(` Snippet length: ${snippetLength} chars`); chatLog.info(` Cached: ${result.cached ? 'Yes' : 'No'}`); if (result.markdown) { const preview = result.markdown.substring(0, 200); chatLog.info(` Preview: ${preview}${contentLength > 200 ? '...' : ''}`); } }); } chatLog.info(`[WebResearch] ============================================\n`); // Adicionar resultados à Knowledge Base if (fallbackResult.scrapedSources.length > 0) { deepResearch.addToKnowledgeBase(query, fallbackResult.scrapedSources); } // Se usou fallback, notificar no frontend if (fallbackResult.usedFallback) { updateThinkingMessage('web-research', { details: `Consulta ${idx + 1}: Nenhum resultado relevante encontrado após ${fallbackResult.attempts.length} tentativas. Usando conhecimento interno.`, }); } // Se houve falhas mas ainda tem resultados, avisar discretamente const failedRounds = fallbackResult.attempts.filter(a => a.results.length === 0); if (failedRounds.length > 0 && fallbackResult.scrapedSources.length > 0) { updateThinkingMessage('web-research', { details: `Consulta ${idx + 1}: Algumas tentativas falharam, mas ${fallbackResult.scrapedSources.length} resultados foram encontrados.`, }); } return { query, results: fallbackResult.scrapedSources, fallbackResult }; } catch (err) { // Logar erro mas continuar (não quebrar o fluxo) const errorMsg = err instanceof Error ? err.message : String(err); chatLog.warn(` ⚠️ Progressive search ${idx + 1} failed (continuando): ${errorMsg}`); // Atualizar mensagem de processo com aviso updateThinkingMessage('web-research', { details: `Consulta ${idx + 1}: Erro na busca (${errorMsg}). Continuando com outras consultas...`, }); return { query, results: [] as ScrapedContent[], fallbackResult: null }; } }); await Promise.all(searchPromises); chatLog.info(`All searches complete. Knowledge Base size: ${deepResearch.state.knowledgeBase.length}`); // Coletar todas as fontes para exibição scrapedSources = deepResearch.state.knowledgeBase.map(entry => ({ url: entry.sourceUrl, title: entry.title, content: entry.content, markdown: entry.content, snippet: entry.content.substring(0, 200), cached: false })); // Adicionar mensagem: Fontes encontradas if (scrapedSources.length > 0) { addThinkingMessage('sources-found', `Fontes encontradas (${scrapedSources.length})`, 'completed'); // Atualizar com fontes updateThinkingMessage('sources-found', { sources: scrapedSources.map(s => ({ url: s.url, title: s.title })), }); } if (deepResearch.state.knowledgeBase.length > 0) { // Passo 3: Validação e Contexto chatLog.info('\n[DeepResearch] ========== STEP 3: VALIDATION =========='); deepResearch.setStep('aggregating'); // Adicionar mensagem: Processamento addThinkingMessage('processing', 'Processando contexto...', 'running'); const validationStart = Date.now(); validationReport = await deepResearch.validate(selectedModel, content); const validationDuration = Date.now() - validationStart; chatLog.info(`[DeepResearch] Validation duration: ${validationDuration}ms`); chatLog.info(`[DeepResearch] Validation report length: ${validationReport.length} chars`); if (validationReport) { const preview = validationReport.substring(0, 500); chatLog.info(`[DeepResearch] Validation preview: ${preview}${validationReport.length > 500 ? '...' : ''}`); } // Passo 4: Obter contexto curado (usando versão otimizada) chatLog.info('\n[DeepResearch] ========== STEP 4: CONTEXT CONDENSATION =========='); // Calcular tokens disponíveis dinamicamente const { getModelContextInfo } = await import('@/lib/model-context'); const contextInfo = await getModelContextInfo(selectedModel, systemPrompt, messages); const availableTokens = contextInfo.recommendedContextWindow; chatLog.info(`[DeepResearch] Model: ${selectedModel}`); chatLog.info(`[DeepResearch] Max context window: ${contextInfo.maxContextWindow} tokens`); chatLog.info(`[DeepResearch] Available tokens for KB: ${availableTokens} tokens`); const condensationStart = Date.now(); const optimizedResult = deepResearch.getCuratedContextOptimized(content, availableTokens); const condensationDuration = Date.now() - condensationStart; knowledgeBaseContext = optimizedResult.context; chatLog.info(`\n[DeepResearch] ========== CONDENSATION SUMMARY ==========`); chatLog.info(`[DeepResearch] Duration: ${condensationDuration}ms`); chatLog.info(`[DeepResearch] Final context length: ${knowledgeBaseContext.length} chars`); chatLog.info(`[DeepResearch] Method: ${optimizedResult.result.method}`); chatLog.info(`[DeepResearch] Chunks used: ${optimizedResult.result.chunksUsed} / ${optimizedResult.result.chunksTotal}`); chatLog.info(`[DeepResearch] Compression ratio: ${(optimizedResult.result.compressionRatio * 100).toFixed(1)}%`); chatLog.info(`[DeepResearch] Original tokens: ${optimizedResult.result.originalTokens}`); chatLog.info(`[DeepResearch] Final tokens: ${optimizedResult.result.totalTokens}`); // Log detalhado por fonte if (optimizedResult.result.sources.length > 0) { chatLog.info(`[DeepResearch] Chunks per source:`); optimizedResult.result.sources.forEach((source, idx) => { chatLog.info(` [${idx + 1}] ${source.title}: ${source.chunksUsed} chunks`); chatLog.info(` URL: ${source.url}`); }); } // Log preview do contexto se não for muito longo if (knowledgeBaseContext.length > 0) { chatLog.info(`\n[DeepResearch] Context preview:`); if (knowledgeBaseContext.length > 2000) { chatLog.info(`${knowledgeBaseContext.substring(0, 2000)}...`); chatLog.info(`[DeepResearch] (truncated, ${knowledgeBaseContext.length} chars total)`); } else { chatLog.info(knowledgeBaseContext); } // Validar contexto const { validateCondensedContext } = await import('@/lib/knowledge-base-processor'); const validation = validateCondensedContext(optimizedResult.result, content); chatLog.info(`\n[DeepResearch] ========== CONTEXT VALIDATION ==========`); if (!validation.isValid) { chatLog.error(`[DeepResearch] Validation failed: ${validation.warnings.join(', ')}`); } else if (validation.warnings.length > 0) { validation.warnings.forEach(warning => { chatLog.warn(`[DeepResearch] ⚠️ ${warning}`); }); } else { chatLog.info(`[DeepResearch] ✓ Context validation passed`); } chatLog.info(`[DeepResearch] ============================================\n`); } else { chatLog.warn('[DeepResearch] ⚠️ Knowledge Base Context is empty!'); } // Atualizar mensagem: Processamento concluído updateThinkingMessage('processing', { status: 'completed', label: 'Contexto processado', }); } else { chatLog.warn('⚠️ No results found in Knowledge Base'); } // Atualizar mensagem: Pesquisa Web concluída updateThinkingMessage('web-research', { status: 'completed', duration: Date.now() - webResearchStart, }); } else { // Fallback para busca simples chatLog.info('Decomposition returned empty, falling back to simple search'); const simpleQuery = await generateQuery(content, selectedModel); chatLog.info(`Simple query generated: "${simpleQuery}"`); if (simpleQuery && simpleQuery !== 'NO_SEARCH') { const results = await webSearch.smartSearchRag(simpleQuery, settings.webSearch.maxResults); chatLog.info(`Simple search results: ${results.length}`); if (results.length > 0) { deepResearch.addToKnowledgeBase(simpleQuery, results); knowledgeBaseContext = deepResearch.getCuratedContext(); scrapedSources = results; } } else { chatLog.info('Simple query returned NO_SEARCH, skipping web search'); } } } catch (error) { // Tratar erro graciosamente sem quebrar o fluxo const errorMsg = error instanceof Error ? error.message : String(error); chatLog.warn(`⚠️ Error in Deep Research pipeline (using local model fallback): ${errorMsg}`); // Atualizar mensagem de processo com aviso (não erro fatal) updateThinkingMessage('web-research', { status: 'completed', // Marcar como completed mesmo com erro details: `Erro na pesquisa web (${errorMsg}). Usando modelo local como fallback.`, duration: Date.now() - webResearchStart, }); // Garantir que o estado de erro seja registrado para ativar fallback // O erro já será detectado pelo hasWebSearchError através do estado do hook chatLog.info('Web search error registered - will use local model fallback'); // Não definir erro global - permitir que o fluxo continue // O sistema continuará com conhecimento interno (fallback automático) } } else { if (!webSearch.isEnabled) { chatLog.info('Web Search disabled, skipping research pipeline'); } else if (!preprocessed.shouldSearch) { chatLog.info(`Intent "${preprocessed.intent}" does not require web search, skipping research pipeline`); } } return { knowledgeBaseContext, scrapedSources, validationReport, }; }; // ========== LÓGICA DE DEEP RESEARCH (Knowledge Base Aggregation) ========== // Removido: setThinkingStep e setProcessSteps - agora usando mensagens thinking agrupadas // Aguardar resultado da busca especulativa se ela foi iniciada e shouldSearch é true let speculativeResults: ScrapedContent[] | null = null; if (speculativeSearchPromise && preprocessed.shouldSearch) { chatLog.info('[SpeculativeSearch] Waiting for speculative search results...'); try { speculativeResults = await speculativeSearchPromise; chatLog.info(`[SpeculativeSearch] Got ${speculativeResults.length} speculative results`); } catch (err) { chatLog.debug(`[SpeculativeSearch] Failed to get speculative results: ${err}`); } } else if (speculativeSearchPromise && !preprocessed.shouldSearch) { chatLog.info('[SpeculativeSearch] Discarding speculative search (preprocessing determined search not needed)'); } // Executar pesquisa web (usando resultados especulativos como seed se disponíveis) const { knowledgeBaseContext, scrapedSources, validationReport } = await executeWebResearch( content, preprocessed, speculativeResults ); // Passo 5: Formular resposta chatLog.info('\nStep 5: Formulating Response'); // Removido: setThinkingStep e setProcessSteps - agora usando mensagens thinking agrupadas // ========== MONTAR SYSTEM PROMPT COM CONTEXTO TEMPORAL E VALIDAÇÃO ========== // Usar timezone do sistema do usuário (não fixo) const now = new Date(); const currentDateTime = now.toLocaleString('pt-BR', { dateStyle: 'full', timeStyle: 'short', // Não especificar timeZone - usar o do sistema }); // Formato adicional mais explícito para garantir que a IA entenda const currentDateISO = now.toISOString(); const currentDateExplicit = `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; let enhancedSystemPrompt = systemPrompt; // Se tivermos uma Knowledge Base, usar o prompt STRICT_GENERATION // Verificar se houve fallback (nenhum resultado relevante encontrado ou erro na pesquisa) const usedFallback = knowledgeBaseContext.length === 0 || knowledgeBaseContext.length < 100; const hasWebSearchError = webSearch.status === 'error' || webSearch.error !== null; if (knowledgeBaseContext && knowledgeBaseContext.length > 100) { const strictPrompt = DEEP_RESEARCH_PROMPTS.STRICT_GENERATION .replace('{{knowledgeBase}}', knowledgeBaseContext) .replace('{{validationReport}}', validationReport || 'Nenhuma validação disponível.') .replace('{{userQuery}}', content); enhancedSystemPrompt = strictPrompt; } else if (usedFallback || hasWebSearchError) { // Fallback: usar prompt sem contexto web, mas informando que não encontrou fontes // Isso garante que o modelo local sempre gere uma resposta mesmo sem dados web const errorContext = hasWebSearchError && webSearch.error ? `\n**Motivo:** ${webSearch.error}` : ''; enhancedSystemPrompt = `${systemPrompt} ## ⚠️ FALLBACK PARA CONHECIMENTO INTERNO Não foi possível acessar fontes web para esta consulta após múltiplas tentativas de busca.${errorContext} **INSTRUÇÕES CRÍTICAS:** - Você DEVE responder usando APENAS seu conhecimento interno (treinamento do modelo) - Seja transparente: informe que não foi possível acessar fontes web, mas que você responderá com base no seu conhecimento - Se tiver conhecimento sobre o tópico, compartilhe-o de forma útil e detalhada - Seja honesto sobre limitações: se não souber algo, diga claramente - NÃO invente informações ou cite fontes que não foram consultadas - NÃO mencione "fontes consultadas" ou "pesquisa realizada" - apenas responda com seu conhecimento **IMPORTANTE:** Este é um fallback legítimo. O usuário espera uma resposta útil mesmo sem dados web. Use seu conhecimento para ajudar da melhor forma possível. ## DATA E HORA ATUAL DO SISTEMA **DATA/HORA FORMATADA:** ${currentDateTime} **DATA/HORA NUMÉRICA:** ${currentDateExplicit} **ISO 8601:** ${currentDateISO} Use esta data exata para referências temporais quando relevante.`; } // Fallback para lógica antiga se não tiver Knowledge Base else if (knowledgeBaseContext) { enhancedSystemPrompt = `${systemPrompt} ## ⚠️ DATA E HORA ATUAL DO SISTEMA (USE ESTA DATA EXATA) **DATA/HORA FORMATADA:** ${currentDateTime} **DATA/HORA NUMÉRICA:** ${currentDateExplicit} **ISO 8601:** ${currentDateISO} **REGRA CRÍTICA:** Você DEVE usar EXATAMENTE esta data e hora. NÃO invente ou calcule datas diferentes. Se o sistema diz que é ${currentDateTime}, então é ${currentDateTime}. Não use "amanhã" ou "ontem" a menos que seja explicitamente solicitado pelo usuário. ## INSTRUÇÕES ESPECÍFICAS PARA RESPOSTAS COM CONTEXTO WEB 1. **Contextualização Temporal Obrigatória:** - Inicie respostas sobre notícias ou eventos citando a data atual EXATA fornecida acima. - Use EXATAMENTE: "${currentDateTime}" (não invente outra data). - Exemplo: "Com base nas notícias de ${currentDateTime}, os principais destaques são..." 2. **Densidade e Profundidade:** - NUNCA responda com listas de tópicos curtos e vagos (ex: "Houve um protesto"). - Cada ponto deve conter detalhes específicos: Números, Nomes Próprios, Locais e Citações Diretas do contexto. - Explique o *contexto* da notícia, não apenas a manchete. - Prefira parágrafos explicativos com "quem, quando, onde, porquê". 3. **Uso Estrito de Fontes:** - Use SOMENTE as informações do bloco [CONTEXTO WEB RECUPERADO] abaixo para fatos recentes. Não alucine. - Se o contexto trouxer múltiplas notícias diferentes, agrupe-as por temas usando títulos Markdown (ex: "## Economia", "## Política"). - NÃO cite fontes no meio do texto. Use as informações do contexto web naturalmente, sem mencionar [1], [2], [3] ou outras referências numéricas. - Se o contexto não for suficiente para responder completamente, diga isso claramente. 4. **Formato Jornalístico:** - Use títulos Markdown (##) para separar seções temáticas. - Texto corrido para explicações detalhadas. - Bullets apenas para listar dados brutos (números, estatísticas). ## CONTEXTO WEB RECUPERADO ${knowledgeBaseContext} --- `; } else { // Mesmo sem contexto web, adicionar data/hora e instruções temporais enhancedSystemPrompt = `${systemPrompt} ## ⚠️ DATA E HORA ATUAL DO SISTEMA (USE ESTA DATA EXATA) **DATA/HORA FORMATADA:** ${currentDateTime} **DATA/HORA NUMÉRICA:** ${currentDateExplicit} **ISO 8601:** ${currentDateISO} **REGRA CRÍTICA:** Você DEVE usar EXATAMENTE esta data e hora. NÃO invente ou calcule datas diferentes. Se o sistema diz que é ${currentDateTime}, então é ${currentDateTime}. Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente a data EXATA fornecida acima (${currentDateTime}). Forneça detalhes ricos (nomes, valores, locais) extraídos do seu conhecimento. Evite resumos de uma linha - prefira explicações detalhadas e contextualizadas. --- `; } // ========== TERMINAL LOGGING: PROMPT INFO ========== chatLog.info('\n--- PROMPT CONSTRUCTION ---'); chatLog.info(`System Prompt Length: ${enhancedSystemPrompt.length} chars`); chatLog.info(`Has Knowledge Base Context: ${knowledgeBaseContext && knowledgeBaseContext.length > 100 ? 'YES' : 'NO'}`); chatLog.info(`Using STRICT_GENERATION prompt: ${knowledgeBaseContext && knowledgeBaseContext.length > 100 ? 'YES' : 'NO'}`); let finalUserContent = content; if (knowledgeBaseContext && knowledgeBaseContext.length > 100) { finalUserContent = `[KNOWLEDGE BASE - ÚNICA FONTE DE VERDADE]\n${knowledgeBaseContext}\n[/KNOWLEDGE BASE]\n\nResponda a pergunta usando APENAS os dados acima. Se a informação não estiver na Knowledge Base, diga "Não encontrei essa informação nas fontes consultadas."\n\nPergunta: ${content}`; } else if (usedFallback || hasWebSearchError) { // Quando usar fallback, garantir que o modelo saiba que deve usar conhecimento interno finalUserContent = `Pergunta do usuário: ${content}\n\nNota: Não foi possível acessar fontes web para esta consulta. Responda usando seu conhecimento interno de forma útil e detalhada.`; } chatLog.info(`Final User Content Length: ${finalUserContent.length} chars`); chatLog.info('\n--- SENDING TO LLM ---'); chatLog.info('Starting response generation...'); // Adicionar mensagem de processo: Geração de Resposta addThinkingMessage('response-generation', 'Gerando resposta...', 'running'); // Adicionar mensagem vazia do assistente para feedback visual imediato // Esta será preenchida quando o streaming começar setMessages(prev => [...prev, { role: 'assistant', content: '' }]); // Enviar o conteúdo original para UI, mas com override no payload para incluir contexto const responseStart = Date.now(); await sendMessage(content, selectedModel, enhancedSystemPrompt, { payloadContentOverride: finalUserContent, }); // Atualizar mensagem de processo: Geração concluída updateThinkingMessage('response-generation', { status: 'completed', duration: Date.now() - responseStart, }); // Removido: setProcessSteps - agora usando mensagens thinking agrupadas chatLog.info('✅ Response generation complete'); chatLog.info('========== QUERY COMPLETE ==========\n'); <<<<<<< HEAD // Capturar dados antes do setMessages para garantir que estão no escopo correto const currentScrapedSources = scrapedSources.length > 0 ? scrapedSources : deepResearch.state.knowledgeBase.map(entry => ({ url: entry.sourceUrl, title: entry.title, content: entry.content, markdown: entry.content, cached: false, })); // Coletar thinking steps das mensagens (apenas os relacionados a esta query) const allThinkingSteps: ThinkingMessageMetadata[] = []; const currentMessages = messages; // Capturar referência atual currentMessages.forEach((msg, idx) => { // Coletar thinking steps das últimas mensagens (relacionadas a esta query) if (idx >= currentMessages.length - 10) { // Últimas 10 mensagens if (msg.metadata && typeof msg.metadata === 'object' && 'type' in msg.metadata && msg.metadata.type === 'thinking') { allThinkingSteps.push(msg.metadata as ThinkingMessageMetadata); } } }); // Coletar sites pesquisados com detalhes completos const sitesResearched = currentScrapedSources.map((source, idx) => { // Tentar encontrar informações de duração e status dos logs const sourceLog = deepResearch.state.logs.find(log => { if (!log.parsedOutput || typeof log.parsedOutput !== 'object') return false; const parsed = log.parsedOutput as Record; return 'url' in parsed && parsed.url === source.url; }); let duration: number | undefined = undefined; if (sourceLog?.parsedOutput && typeof sourceLog.parsedOutput === 'object') { const parsed = sourceLog.parsedOutput as Record; if ('duration' in parsed && typeof parsed.duration === 'number') { duration = parsed.duration; } } return { url: source.url, title: source.title || 'Sem título', status: source.cached ? 'cached' : 'scraped', source: source.cached ? 'cache' : 'nodriver', contentLength: source.content?.length || source.markdown?.length || 0, scrapedAt: sourceLog?.timestamp || Date.now(), duration: duration, }; }); // Coletar dados de raciocínio dos logs do deep research const reasoningSteps = deepResearch.state.logs .filter(log => log.stage === 'generation' || log.stage === 'validation') .map(log => ({ stage: log.stage, input: log.input, output: log.rawOutput || JSON.stringify(log.parsedOutput), timestamp: log.timestamp, })); // Estimar token usage (aproximado) const estimatedInputTokens = Math.ceil((enhancedSystemPrompt.length + finalUserContent.length) / 4); const lastAssistantMsg = currentMessages.find(m => m.role === 'assistant'); const estimatedOutputTokens = Math.ceil((lastAssistantMsg?.content?.length || 0) / 4); ======= >>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 // Construir dados de debug para a última mensagem do assistente setMessages(prev => { const newMessages = [...prev]; const lastMsgIndex = newMessages.length - 1; if (lastMsgIndex >= 0 && newMessages[lastMsgIndex].role === 'assistant') { const lastMsg = newMessages[lastMsgIndex]; const debugData: DebugData = { model: selectedModel, timestamp: Date.now(), latency: Date.now() - responseStart, systemPrompt: enhancedSystemPrompt, userQuery: content, <<<<<<< HEAD contextUsed: prev.filter(m => m.role !== 'system' && m.role !== 'assistant'), webResearch: { queries: deepResearch.state.plan || [], enrichedQueries: deepResearch.state.enrichedQueries || undefined, sources: currentScrapedSources, sitesResearched: sitesResearched, logs: deepResearch.state.logs || [], plan: deepResearch.state.plan || [], ======= contextUsed: messages.filter(m => m.role !== 'system'), webResearch: { queries: deepResearch.state.plan || [], enrichedQueries: deepResearch.state.enrichedQueries || undefined, sources: scrapedSources, logs: deepResearch.state.logs, plan: deepResearch.state.plan, >>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 knowledgeBase: deepResearch.state.knowledgeBase.map(entry => ({ sourceUrl: entry.sourceUrl, title: entry.title, content: entry.content, })), <<<<<<< HEAD activeQueries: webSearch.activeQueries.length > 0 ? webSearch.activeQueries : undefined, activeUrls: webSearch.activeUrls.length > 0 ? webSearch.activeUrls : undefined, }, deepResearchState: deepResearch.state, thinkingSteps: allThinkingSteps.length > 0 ? allThinkingSteps : undefined, reasoning: reasoningSteps.length > 0 ? { steps: reasoningSteps.map(r => `${r.stage}: ${r.input.substring(0, 100)}...`), intermediateResults: reasoningSteps.map(r => ({ stage: r.stage, output: r.output, timestamp: r.timestamp, })), } : undefined, finalResponse: lastMsg.content || '', rawResponse: lastMsg.content || '', tokenUsage: { input: estimatedInputTokens, output: estimatedOutputTokens, total: estimatedInputTokens + estimatedOutputTokens, }, ======= }, deepResearchState: deepResearch.state, finalResponse: lastMsg.content, rawResponse: lastMsg.content, >>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 }; // Armazenar debug data usando o índice da mensagem setDebugDataMap(prevMap => { const newMap = new Map(prevMap); newMap.set(lastMsgIndex, debugData); return newMap; }); } return newMessages; }); // Persistir fontes na mensagem do assistente (metadata) if (scrapedSources.length > 0) { // Aguardar um pouco para garantir que o sendMessage iniciou e criou a mensagem do assistente // O sendMessage é async mas retorna void enquanto a stream acontece. // O setMessages dentro dele adiciona a mensagem do assistente. // Nota: Como sendMessage roda em background (streaming), não podemos garantir que a mensagem // do assistente já existe aqui imediatamente se não esperarmos. // Mas o sendMessage faz `setMessages` com assistant vazio logo no início. // Vamos atualizar o estado usando callback para garantir que pegamos o mais recente setMessages(prev => { const newMessages = [...prev]; const lastMsgIndex = newMessages.length - 1; if (lastMsgIndex >= 0 && newMessages[lastMsgIndex].role === 'assistant') { const lastMsg = newMessages[lastMsgIndex]; newMessages[lastMsgIndex] = { ...lastMsg, metadata: { ...(lastMsg.metadata || {}), sources: scrapedSources } }; } return newMessages; }); } // Removido: setThinkingStep - agora usando mensagens thinking agrupadas }; const handleSelectSession = async (id: string) => { if (id === currentSessionId) return; // Clear any pending saves if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = null; } const history = await loadSessionHistory(id); 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 } }; // Handler para scroll infinito reverso (carregar mensagens mais antigas) const handleScroll = useCallback(async () => { const container = scrollContainerRef.current; if (!container || !currentSessionId || paginationState.isLoadingMore) return; // Verificar se o usuário está próximo do topo (threshold de 100px) const isNearTop = container.scrollTop < 100; if (isNearTop && paginationState.hasMore) { // Salvar posição atual do scroll e altura do conteúdo const scrollHeightBefore = container.scrollHeight; // Carregar mais mensagens const olderMessages = await loadMoreMessages(currentSessionId); if (olderMessages.length > 0) { // Prepend as mensagens antigas setMessages(prev => [...olderMessages, ...prev]); // Restaurar posição do scroll para manter o contexto visual // (compensar o novo conteúdo adicionado no topo) requestAnimationFrame(() => { if (container) { const scrollHeightAfter = container.scrollHeight; const heightDiff = scrollHeightAfter - scrollHeightBefore; container.scrollTop = container.scrollTop + heightDiff; } }); } } }, [currentSessionId, paginationState.hasMore, paginationState.isLoadingMore, loadMoreMessages, setMessages]); // 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(); clearChat(); setCurrentSessionId(null); // Reset save tracking if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = null; } lastSavedMessagesRef.current = 0; }; const toggleChatsSidebar = () => { const panel = chatsSidebarRef.current; if (panel) { if (isChatsSidebarCollapsed) { // No mobile, apenas atualizar estado (overlay será mostrado) if (typeof window !== 'undefined' && window.innerWidth < 768) { setIsChatsSidebarCollapsed(false); } else { panel.expand(); } } else { panel.collapse(); } } }; return (
{/* Custom Title Bar */} {/* Main Content Area */}
{/* Sessions List - Desktop: ResizablePanel, Mobile: Overlay */} setIsChatsSidebarCollapsed(true)} onExpand={() => setIsChatsSidebarCollapsed(false)} className="min-w-0 md:min-w-[220px] md:max-w-[360px] hidden md:block" style={{ minWidth: 'clamp(220px, 20vw, 360px)', maxWidth: '360px', }} > { handleSelectSession(id); // Fechar sidebar no mobile após seleção if (typeof window !== 'undefined' && window.innerWidth < 768) { setIsChatsSidebarCollapsed(true); chatsSidebarRef.current?.collapse(); } }} onDeleteSession={deleteSession} onNewChat={handleNewChat} onSearch={searchSessions} isSearching={isSearching} searchQuery={searchQuery} onNavigateMatch={handleNavigateMatch} /> {/* Main Chat Area */}
{/* Header */}
Chat {isGeneratingTitle && } {isDownloading && ( {progress} )}
{/* Messages Area with Floating Input Layout */}
{/* Scrollable Content Area */}
{/* Indicador de carregamento de mensagens antigas */} {paginationState.isLoadingMore && (
Carregando mensagens anteriores...
)} {/* Indicador de que há mais mensagens para carregar */} {paginationState.hasMore && !paginationState.isLoadingMore && messages.length > 0 && (
Role para cima para ver mensagens anteriores ({paginationState.totalCount - messages.length} restantes)
)} {messages.length === 0 ? (

Como posso ajudar você hoje?

Selecione um modelo e comece uma conversa.

) : (
{(() => { // Agrupar mensagens thinking consecutivas const groupedMessages: Array<{ type: 'thinking-group' | 'regular'; messages?: Message[]; thinkingSteps?: ThinkingMessageMetadata[]; searchQueries?: string[]; index?: number; message?: Message; isStreaming?: boolean; }> = []; let currentThinkingGroup: ThinkingMessageMetadata[] = []; let currentSearchQueries: string[] = []; for (let i = 0; i < messages.length; i++) { const msg = messages[i]; const isThinkingMessage = msg.metadata && typeof msg.metadata === 'object' && 'type' in msg.metadata && msg.metadata.type === 'thinking'; // Pular mensagens de sistema que não são thinking (system prompt) // Essas mensagens nunca devem aparecer no chat const isSystemPrompt = msg.role === 'system' && !isThinkingMessage; if (isSystemPrompt) { continue; } if (isThinkingMessage) { const thinkingMeta = msg.metadata as ThinkingMessageMetadata; currentThinkingGroup.push(thinkingMeta); // Extrair queries de busca dos detalhes if (thinkingMeta.details && thinkingMeta.stepType === 'web-research') { const queryMatch = thinkingMeta.details.match(/Buscando consulta \d+\/\d+: "([^"]+)"/); if (queryMatch && queryMatch[1]) { if (!currentSearchQueries.includes(queryMatch[1])) { currentSearchQueries.push(queryMatch[1]); } } } } else { // Se havia um grupo thinking, adicionar antes desta mensagem if (currentThinkingGroup.length > 0) { groupedMessages.push({ type: 'thinking-group', thinkingSteps: [...currentThinkingGroup], searchQueries: [...currentSearchQueries], }); currentThinkingGroup = []; currentSearchQueries = []; } // Adicionar mensagem regular const isAssistantStreaming = i === messages.length - 1 && msg.role === 'assistant' && isLoading; groupedMessages.push({ type: 'regular', message: msg, index: i, isStreaming: isAssistantStreaming, }); } } // Adicionar grupo thinking final se houver if (currentThinkingGroup.length > 0) { groupedMessages.push({ type: 'thinking-group', thinkingSteps: currentThinkingGroup, searchQueries: currentSearchQueries, }); } // 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 (
); } 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 (
= 2 ? searchQuery.trim() : undefined} highlightIndex={messageMatchIndex} messageIndex={group.index} debugData={group.index !== undefined ? debugDataMap.get(group.index) : undefined} />
); } return null; }); })()}
)} {/* Spacer for Bottom Input */}
{/* Fixed Input Container */}
{/* Gradient Overlay */}
{/* Input Wrapper */}
{ const cat = settings.webSearch.categories.find(c => c.id === id); if (!cat) return; settings.updateCategory({ ...cat, enabled }); }} />

OllaHub pode cometer erros. Verifique informações importantes.

{/* Mobile Sidebar Overlay */} {!isChatsSidebarCollapsed && ( <>
{ setIsChatsSidebarCollapsed(true); chatsSidebarRef.current?.collapse(); }} />
{ handleSelectSession(id); setIsChatsSidebarCollapsed(true); }} onDeleteSession={deleteSession} onNewChat={() => { handleNewChat(); setIsChatsSidebarCollapsed(true); }} />
)} { setShowDownloadDialog(open); // Se fechando, garantir que o Select não mantenha o valor especial if (!open && selectedModel === "__add_model__") { setSelectedModel(""); } }} onSuccess={(modelName) => { // Atualizar lista de modelos refresh(); // Selecionar o modelo recém-baixado após um pequeno delay para garantir que a lista foi atualizada setTimeout(() => { setSelectedModel(modelName); settings.setSelectedModel(modelName); }, 500); }} />
); }