From 84ea2dec19c177d8148b5bbba819198fd90910a2 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Wed, 26 Nov 2025 16:12:16 -0300 Subject: [PATCH] feat: add emoji-regex and remove-accents dependencies; enhance chat and setup pages - Added `emoji-regex` and `remove-accents` packages to improve text processing capabilities. - Implemented automatic Ollama startup in the chat page for better user experience. - Enhanced setup page to include GPU selection based on hardware specifications. - Updated chat input to support real-time validation and improved user feedback. - Refactored message handling in chat to incorporate new processing features. --- app/chat/page.tsx | 998 +++++++++++++----- app/page.tsx | 25 + app/setup/page.tsx | 9 +- components/chat/chat-input.tsx | 156 ++- components/chat/chat-message.tsx | 136 ++- components/chat/error-display.tsx | 64 ++ components/chat/message-background.tsx | 18 +- components/chat/message-step-thinking.tsx | 175 +++ components/chat/model-download-dialog.tsx | 367 +++++++ components/chat/post-processing-panel.tsx | 127 +++ components/chat/processing-indicator.tsx | 192 ++++ components/chat/sources-display.tsx | 120 +++ components/chat/streaming-indicator.tsx | 43 + components/chat/thinking-indicator.tsx | 22 +- components/modals/install-modal.tsx | 279 ++++- .../notifications/welcome-notification.tsx | 67 ++ components/setup/gpu-selector.tsx | 139 +++ components/setup/model-selector.tsx | 85 +- data/prompts/batch-verify.ts | 46 + data/prompts/deep-research.ts | 67 ++ hooks/use-batch-verify.ts | 190 ++++ hooks/use-chat.ts | 393 ++++--- hooks/use-deep-research.ts | 524 +++++++++ hooks/use-hardware.ts | 8 +- hooks/use-ollama-check.ts | 47 +- hooks/use-ollama-download.ts | 257 +++++ hooks/use-operating-system.ts | 56 + hooks/use-query-preprocessor.ts | 182 ++++ lib/content-condenser.ts | 387 +++++++ lib/download-utils.ts | 97 ++ lib/knowledge-base-processor.ts | 261 +++++ lib/model-context.ts | 133 +++ lib/query-validation.ts | 177 ++++ lib/question-splitter.ts | 217 ++++ lib/recommendation.ts | 8 + lib/relevance-scorer.ts | 193 ++++ lib/terminal-logger.ts | 38 + lib/text-normalization.ts | 81 ++ lib/web-search-fallback.ts | 298 ++++++ package.json | 2 + pnpm-lock.yaml | 16 + pnpm-workspace.yaml | 3 + public/installers/.gitkeep | 9 + src-tauri/Cargo.lock | 22 + src-tauri/Cargo.toml | 1 + src-tauri/src/intent_classifier.rs | 208 ++++ src-tauri/src/lib.rs | 673 +++++++++++- src-tauri/src/scheduler.rs | 1 + src-tauri/src/system_monitor.rs | 518 ++++++++- src-tauri/src/web_scraper.rs | 159 ++- store/settings-store.ts | 136 +++ 51 files changed, 7857 insertions(+), 573 deletions(-) create mode 100644 components/chat/error-display.tsx create mode 100644 components/chat/message-step-thinking.tsx create mode 100644 components/chat/model-download-dialog.tsx create mode 100644 components/chat/post-processing-panel.tsx create mode 100644 components/chat/processing-indicator.tsx create mode 100644 components/chat/sources-display.tsx create mode 100644 components/chat/streaming-indicator.tsx create mode 100644 components/notifications/welcome-notification.tsx create mode 100644 components/setup/gpu-selector.tsx create mode 100644 data/prompts/batch-verify.ts create mode 100644 data/prompts/deep-research.ts create mode 100644 hooks/use-batch-verify.ts create mode 100644 hooks/use-deep-research.ts create mode 100644 hooks/use-ollama-download.ts create mode 100644 hooks/use-operating-system.ts create mode 100644 hooks/use-query-preprocessor.ts create mode 100644 lib/content-condenser.ts create mode 100644 lib/download-utils.ts create mode 100644 lib/knowledge-base-processor.ts create mode 100644 lib/model-context.ts create mode 100644 lib/query-validation.ts create mode 100644 lib/question-splitter.ts create mode 100644 lib/relevance-scorer.ts create mode 100644 lib/terminal-logger.ts create mode 100644 lib/text-normalization.ts create mode 100644 lib/web-search-fallback.ts create mode 100644 pnpm-workspace.yaml create mode 100644 public/installers/.gitkeep create mode 100644 src-tauri/src/intent_classifier.rs diff --git a/app/chat/page.tsx b/app/chat/page.tsx index eac862d..96c9d56 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; -import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown } from "lucide-react"; +import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, ScrollText, Copy, Check } from "lucide-react"; import { ChatInput } from "@/components/chat/chat-input"; import { ChatMessage } from "@/components/chat/chat-message"; @@ -15,17 +15,25 @@ import { useLocalModels } from "@/hooks/use-local-models"; import { useChatStorage } from "@/hooks/use-chat-storage"; import { useAutoLabelingModel } from "@/hooks/use-auto-labeling-model"; import { useWebSearch } from "@/hooks/use-web-search"; -import { SearchProgress } from "@/components/chat/search-progress"; -import { ThinkingIndicator, ThinkingStep } from "@/components/chat/thinking-indicator"; +// 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 { invoke } from "@tauri-apps/api/core"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; import { ImperativePanelHandle } from "react-resizable-panels"; +import { ModelDownloadDialog } from "@/components/chat/model-download-dialog"; import { sanitizeWebSources } from "@/lib/sanitize-web-content"; +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, type FallbackResult } from "@/lib/web-search-fallback"; // @ts-ignore import defaultFormatPrompt from "@/data/prompts/default-format.md"; @@ -50,7 +58,62 @@ export default function ChatPage() { const { isDownloading, progress } = useAutoLabelingModel(); const webSearch = useWebSearch(); const { generateQuery, isGenerating: isGeneratingQuery } = 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; + }, []); + + 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; + })); + }, []); + + const removeThinkingMessage = useCallback((stepType: ThinkingStepType) => { + setMessages(prev => prev.filter(msg => { + const metadata = msg.metadata as ThinkingMessageMetadata | undefined; + return !(metadata?.type === 'thinking' && metadata.stepType === stepType); + })); + }, []); const [selectedModel, setSelectedModel] = useState(""); const [mounted, setMounted] = useState(false); @@ -59,14 +122,20 @@ export default function ChatPage() { const [systemPrompt, setSystemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo."); const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(false); const [thinkingStep, setThinkingStep] = useState(null); + const [processSteps, setProcessSteps] = useState([]); + const [error, setError] = useState(null); const chatsSidebarRef = useRef(null); const saveTimeoutRef = useRef(null); const lastSavedMessagesRef = useRef(0); const messagesEndRef = useRef(null); const [showContextDebug, setShowContextDebug] = useState(false); + const [showProcessDebug, setShowProcessDebug] = useState(false); const [lastWebContext, setLastWebContext] = useState(''); const [lastContextSources, setLastContextSources] = useState([]); + const [showDownloadDialog, setShowDownloadDialog] = useState(false); + const [logsCopied, setLogsCopied] = useState(false); + const [lastUserQuery, setLastUserQuery] = useState(''); // Auto-select first model useEffect(() => { @@ -133,96 +202,441 @@ export default function ChatPage() { }, [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); + 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]); + 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'); + + // Resetar e inicializar steps do processo (para compatibilidade com componentes antigos) + setProcessSteps([ + { id: 'preprocessing', label: 'Pré-processamento', status: 'running', timestamp: Date.now() }, + ]); + + // Limpar erros anteriores + setError(null); + + // ========== 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 errorMessage = error instanceof Error ? error.message : String(error); + updateThinkingMessage('preprocessing', { + status: 'error', + error: errorMessage, + duration: Date.now() - preprocessingStart, + }); + + setProcessSteps(prev => prev.map(s => + s.id === 'preprocessing' + ? { ...s, status: 'error' as const, error: errorMessage } + : s + )); + + // Definir erro global + setError(error); + + chatLog.error('Preprocessing failed:', error); + // 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()}`); + setShowContextDebug(false); + setShowProcessDebug(false); setLastWebContext(''); setLastContextSources([]); + setLastUserQuery(content); // Store for copy logs + setLogsCopied(false); // Generate ID if new session (but don't create card yet - wait for title) if (!currentSessionId) { const newId = crypto.randomUUID(); setCurrentSessionId(newId); - // Card will be created after title is generated (after first AI response) + chatLog.info(`New session created: ${newId}`); } - // ========== LÓGICA DE AGENTE DE PESQUISA ========== - let webContext = ''; - let searchQuery = ''; - let scrapedSources: ScrapedContent[] = []; - - // Iniciar indicador de pensamento - setThinkingStep('analyzing'); - - if (webSearch.isEnabled) { + // Função auxiliar para executar pesquisa web + const executeWebResearch = async ( + content: string, + preprocessed: PreprocessedQuery + ): 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 ---'); + + // Adicionar mensagem de processo: Pesquisa Web + addThinkingMessage('web-research', 'Pesquisando na Web...', 'running'); + const webResearchStart = Date.now(); + try { - webSearch.reset(); // Resetar estado anterior + webSearch.reset(); + deepResearch.reset(); + + // Passo 1: Decomposição + chatLog.info('Step 1: Decomposition'); + setThinkingStep('planning'); + const searchPlan = await deepResearch.decompose(content, selectedModel); + chatLog.info(`Search Plan: ${JSON.stringify(searchPlan)}`); - // Passo 1: Gerar query otimizada - setThinkingStep('analyzing'); - searchQuery = await generateQuery(content, selectedModel); - - // Passo 2: Se precisar de busca, executar - if (searchQuery && searchQuery !== 'NO_SEARCH') { - setThinkingStep('searching'); - // Pipeline em duas etapas: metadados → scraping de top URLs - const maxResults = settings.webSearch.maxResults; - scrapedSources = await webSearch.smartSearchRag(searchQuery, maxResults); - - // Atualizar indicador enquanto lê fontes - setThinkingStep('reading'); - - if (scrapedSources.length > 0) { - // Sanitizar e combinar markdown de todas as fontes - // Priorizamos o markdown completo (não apenas snippets) para garantir densidade de informação - // O scraper Rust já filtra conteúdo muito curto (< 200 chars) automaticamente - // Sanitização previne prompt injection e limita tamanho - webContext = sanitizeWebSources(scrapedSources, { - maxLength: 8000, // ~8k chars por fonte (ajustável) - removeControlChars: true, - removeHiddenText: true, - }); - - // [DEBUG INJECTION START] - console.group('🔍 Debug: Web Context Generation'); - console.log('1. Raw Sources Count:', scrapedSources.length); - console.log('2. Web Context Length:', webContext.length); - console.log('3. Is Context Empty?', webContext === ''); - if (webContext.length > 0) { - console.log('4. Context Preview:', webContext.substring(0, 200) + '...'); - } else { - console.warn('⚠️ ALERT: Web Context is EMPTY even with sources!'); - console.log('4. Sources Details:', scrapedSources.map(s => ({ - url: s.url, - title: s.title, - hasMarkdown: !!s.markdown, - markdownLength: s.markdown?.length || 0 - }))); - } - console.groupEnd(); - // [DEBUG INJECTION END] - - // Log para debug: verificar tamanho do contexto gerado - const contextLength = webContext.length; - console.debug(`Contexto web gerado (sanitizado): ${contextLength} caracteres de ${scrapedSources.length} fontes`); - setLastWebContext(webContext); - setLastContextSources(scrapedSources); - setShowContextDebug(false); - } + if (searchPlan && searchPlan.length > 0) { + // Passo 2: Executar buscas com fallback progressivo + chatLog.info(`\nStep 2: Executing ${searchPlan.length} searches with progressive fallback`); + setThinkingStep('searching'); + + // 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) => { + chatLog.info(` → Starting progressive search ${idx + 1}/${searchPlan.length}: "${query}"`); + + // Atualizar progresso + updateThinkingMessage('web-research', { + details: `Buscando consulta ${idx + 1}/${searchPlan.length}: "${query}"...`, + progress: 10 + (idx * 20 / searchPlan.length), + }); + + try { + // Executar busca progressiva + const fallbackResult = await executeProgressiveSearch( + query, + async (q, limit) => { + return await webSearch.smartSearchRag(q, limit); + }, + selectedModel, + { + maxRounds: 3, // Máximo 3 rodadas por query + maxResultsPerRound: maxResults, + maxTotalResults: 30, // Máximo 30 resultados por query + minRelevanceScore: 0.3, + enableQueryExpansion: true, + } + ); + + // Log do resultado + chatLog.info(` ✓ Progressive search ${idx + 1} complete:`); + chatLog.info(` - Success: ${fallbackResult.success}`); + chatLog.info(` - Total results: ${fallbackResult.scrapedSources.length}`); + chatLog.info(` - Total analyzed: ${fallbackResult.totalResultsAnalyzed}`); + chatLog.info(` - Attempts: ${fallbackResult.attempts.length}`); + chatLog.info(` - Used fallback: ${fallbackResult.usedFallback}`); + + // Log detalhado de cada resultado + fallbackResult.scrapedSources.forEach((result, resultIdx) => { + chatLog.info(` → Result ${resultIdx + 1}: ${result.title}`); + chatLog.info(` URL: ${result.url}`); + const contentLength = result.markdown?.length || 0; + chatLog.info(` Content length: ${contentLength} chars`); + }); + + // 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.`, + }); + } + + return { query, results: fallbackResult.scrapedSources, fallbackResult }; + } catch (err) { + chatLog.error(` ✗ Progressive search ${idx + 1} failed: ${err}`); + 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, + markdown: entry.content, + snippet: entry.content.substring(0, 200) + })); + + // 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('\nStep 3: Validation'); + deepResearch.setStep('aggregating'); + + // Adicionar mensagem: Processamento + addThinkingMessage('processing', 'Processando contexto...', 'running'); + validationReport = await deepResearch.validate(selectedModel, content); + chatLog.info(`Validation Report Length: ${validationReport.length} chars`); + + // Passo 4: Obter contexto curado (usando versão otimizada) + chatLog.info('\nStep 4: Getting Curated Context (Optimized)'); + + // 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(`Model Context Window: ${contextInfo.maxContextWindow} tokens`); + chatLog.info(`Available Tokens for KB: ${availableTokens} tokens`); + + const optimizedResult = deepResearch.getCuratedContextOptimized(content, availableTokens); + knowledgeBaseContext = optimizedResult.context; + + chatLog.info(`Knowledge Base Context Length: ${knowledgeBaseContext.length} chars`); + chatLog.info(`Condensation Method: ${optimizedResult.result.method}`); + chatLog.info(`Chunks Used: ${optimizedResult.result.chunksUsed} / ${optimizedResult.result.chunksTotal}`); + chatLog.info(`Compression Ratio: ${(optimizedResult.result.compressionRatio * 100).toFixed(1)}%`); + + // Log preview do contexto se não for muito longo + if (knowledgeBaseContext.length > 0) { + if (knowledgeBaseContext.length > 2000) { + chatLog.info(`Context Preview (first 2000 chars):\n${knowledgeBaseContext.substring(0, 2000)}...`); + } else { + chatLog.info(`Full Context:\n${knowledgeBaseContext}`); + } + + // Validar contexto + const { validateCondensedContext } = await import('@/lib/knowledge-base-processor'); + const validation = validateCondensedContext(optimizedResult.result, content); + + if (!validation.isValid) { + chatLog.error('Context validation failed:', validation.warnings.join(', ')); + } else if (validation.warnings.length > 0) { + validation.warnings.forEach(warning => { + chatLog.warn(`⚠️ ${warning}`); + }); + } else { + chatLog.info('✓ Context validation passed'); + } + } else { + chatLog.warn('⚠️ Knowledge Base Context is empty!'); + } + + setLastWebContext(knowledgeBaseContext); + setLastContextSources(scrapedSources); + + // 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 { - setLastWebContext(''); - setLastContextSources([]); + // Fallback para busca simples + chatLog.info('Decomposition returned empty, falling back to simple search'); + setThinkingStep('searching'); + 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}`); + setThinkingStep('reading'); + if (results.length > 0) { + deepResearch.addToKnowledgeBase(simpleQuery, results); + knowledgeBaseContext = deepResearch.getCuratedContext(); + scrapedSources = results; + setLastWebContext(knowledgeBaseContext); + setLastContextSources(scrapedSources); + } + } else { + chatLog.info('Simple query returned NO_SEARCH, skipping web search'); + } } + } catch (error) { - console.error('Erro na pesquisa web:', error); - // Continuar mesmo se a pesquisa falhar + const errorMsg = error instanceof Error ? error.message : String(error); + chatLog.error(`❌ Error in Deep Research pipeline: ${errorMsg}`); + + // Atualizar mensagem de processo com erro + updateThinkingMessage('web-research', { + status: 'error', + error: errorMsg, + duration: Date.now() - webResearchStart, + }); + + // Atualizar step com erro (para compatibilidade) + setProcessSteps(prev => prev.map(s => + s.id === 'web-research' + ? { ...s, status: 'error' as const, error: errorMsg } + : s + )); + + // Definir erro global + setError(error); + } + } 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`); } } - // Passo 3: Formular resposta + return { + knowledgeBaseContext, + scrapedSources, + validationReport, + }; + }; + + // ========== LÓGICA DE DEEP RESEARCH (Knowledge Base Aggregation) ========== + // Iniciar indicador de pensamento + setThinkingStep('analyzing'); + + // Adicionar step de pesquisa web se necessário + if (webSearch.isEnabled && preprocessed.shouldSearch) { + setProcessSteps(prev => [ + ...prev, + { id: 'web-research', label: 'Pesquisa Web', status: 'running', timestamp: Date.now() }, + ]); + } + + const researchStart = Date.now(); + // Executar pesquisa web + const { knowledgeBaseContext, scrapedSources, validationReport } = await executeWebResearch( + content, + preprocessed + ); + + // Atualizar step de pesquisa web + if (webSearch.isEnabled && preprocessed.shouldSearch) { + setProcessSteps(prev => prev.map(s => + s.id === 'web-research' + ? { ...s, status: 'completed' as const, duration: Date.now() - researchStart } + : s + )); + } + + // Passo 5: Formular resposta + chatLog.info('\nStep 5: Formulating Response'); setThinkingStep('formulating'); - // ========== MONTAR SYSTEM PROMPT COM CONTEXTO TEMPORAL ========== + // Adicionar step de geração de resposta + setProcessSteps(prev => [ + ...prev, + { id: 'response-generation', label: 'Geração de Resposta', status: 'running', timestamp: Date.now() }, + ]); + + // ========== 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', { @@ -237,8 +651,39 @@ export default function ChatPage() { let enhancedSystemPrompt = systemPrompt; - // Adicionar contexto temporal e web - if (webContext) { + // Se tivermos uma Knowledge Base, usar o prompt STRICT_GENERATION + // Verificar se houve fallback (nenhum resultado relevante encontrado) + const usedFallback = knowledgeBaseContext.length === 0 || knowledgeBaseContext.length < 100; + + 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) { + // Fallback: usar prompt sem contexto web, mas informando que não encontrou fontes + enhancedSystemPrompt = `${systemPrompt} + +## ⚠️ AVISO IMPORTANTE + +Não foi possível encontrar informações relevantes nas fontes web consultadas após múltiplas tentativas de busca. + +**INSTRUÇÕES:** +- Responda usando APENAS seu conhecimento interno (treinamento) +- Seja honesto e diga: "Não encontrei essa informação nas fontes consultadas." +- Se tiver conhecimento sobre o tópico, compartilhe, mas deixe claro que não há fontes externas verificadas +- Não invente informações ou cite fontes que não foram consultadas + +## DATA E HORA ATUAL DO SISTEMA + +**DATA/HORA FORMATADA:** ${currentDateTime} + +Use esta data exata para referências temporais.`; + } + // 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) @@ -274,7 +719,7 @@ export default function ChatPage() { - Bullets apenas para listar dados brutos (números, estatísticas). ## CONTEXTO WEB RECUPERADO -${webContext} +${knowledgeBaseContext} --- `; @@ -296,26 +741,50 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente `; } - // [DEBUG INJECTION START] - console.log('5. Enhanced System Prompt Length:', enhancedSystemPrompt.length); - console.log('6. Contains "CONTEXTO WEB"?', enhancedSystemPrompt.includes('CONTEXTO WEB RECUPERADO')); - if (enhancedSystemPrompt.includes('CONTEXTO WEB RECUPERADO')) { - const contextStart = enhancedSystemPrompt.indexOf('CONTEXTO WEB RECUPERADO'); - console.log('7. Web Context Position:', contextStart); - console.log('8. Web Context in Prompt Preview:', enhancedSystemPrompt.substring(contextStart, contextStart + 300) + '...'); - } - // [DEBUG INJECTION END] + // ========== 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 (webContext) { - finalUserContent = `[CONTEXTO WEB OBRIGATÓRIO]\n${webContext}\n[/CONTEXTO WEB]\n\nCom base EXCLUSIVAMENTE no texto acima, responda: ${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}`; } + 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, + }); + + // Atualizar step de geração de resposta quando streaming completar (para compatibilidade) + setProcessSteps(prev => prev.map(s => + s.id === 'response-generation' + ? { ...s, status: 'completed' as const, duration: Date.now() - responseStart } + : s + )); + + chatLog.info('✅ Response generation complete'); + chatLog.info('========== QUERY COMPLETE ==========\n'); + // 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 @@ -387,6 +856,46 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente } }; + // Copy Logs to Clipboard + const handleCopyLogs = async () => { + const logs = deepResearch.state.logs; + const lastAssistantMessage = messages.filter(m => m.role === 'assistant').pop(); + + let logText = `# Deep Research Debug Log\n\n`; + logText += `## User Query\n${lastUserQuery}\n\n`; + logText += `## Model\n${selectedModel}\n\n`; + logText += `## Process Logs\n\n`; + + logs.forEach((log, idx) => { + logText += `### ${idx + 1}. Stage: ${log.stage.toUpperCase()}\n`; + logText += `- **Time**: ${new Date(log.timestamp).toLocaleTimeString()}\n`; + logText += `- **Input**: ${log.input.substring(0, 500)}${log.input.length > 500 ? '...' : ''}\n`; + if (log.rawOutput) { + logText += `- **Raw Output**: ${log.rawOutput.substring(0, 500)}${log.rawOutput.length > 500 ? '...' : ''}\n`; + } + if (log.parsedOutput) { + logText += `- **Parsed Output**: \`\`\`json\n${JSON.stringify(log.parsedOutput, null, 2)}\n\`\`\`\n`; + } + if (log.error) { + logText += `- **Error**: ${log.error}\n`; + } + logText += '\n'; + }); + + logText += `## AI Response\n`; + logText += lastAssistantMessage?.content || '[No response yet]'; + logText += '\n\n---\n'; + logText += `Generated at: ${new Date().toISOString()}`; + + try { + await navigator.clipboard.writeText(logText); + setLogsCopied(true); + setTimeout(() => setLogsCopied(false), 2000); + } catch (err) { + console.error('Failed to copy logs:', err); + } + }; + return (
{/* Left Sidebar (Nav) */} @@ -477,7 +986,18 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
- { + if (v === "__add_model__") { + setShowDownloadDialog(true); + // Não alterar selectedModel quando abrir dialog + } else { + setSelectedModel(v); + settings.setSelectedModel(v); + } + }} + > @@ -490,6 +1010,12 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente ))} + + + + Adicionar modelo... + +
@@ -499,167 +1025,167 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
- {/* Messages */} -
- {messages.length === 0 ? ( -
-
- -
-

Inicie uma conversa com {selectedModel}

-
- ) : ( -
- {messages.map((msg, i) => { - // Verificar fontes nos metadados (preferencial) - const msgSources = msg.metadata?.sources || []; - const hasWebSources = msgSources.length > 0; - - // Verificar se devemos mostrar o progresso em tempo real - // Apenas para a interação atual (quando isLoading é true ou acabou de terminar) - const isCurrentInteraction = i === messages.length - (isLoading ? 2 : 1) || i === messages.length - 1; - const showRealtimeProgress = - msg.role === 'user' && - isCurrentInteraction && - (webSearch.status === 'searching' || - webSearch.status === 'scraping' || - webSearch.status === 'completed' || - webSearch.status === 'error') && - webSearch.currentQuery; // Só mostra se tiver query - - return ( -
- - {(msg.role === 'user') && showRealtimeProgress && ( - - )} - {/* Mostrar fontes após resposta do assistente que usou web search */} - {msg.role === 'assistant' && hasWebSources && ( -
-
-

- Fontes consultadas: -

- -
-
- )} + {/* Messages Area with Floating Input Layout */} +
+ {/* Scrollable Content Area */} +
+
+ {messages.length === 0 ? ( +
+
+
- ); - })} - - {isLoading && thinkingStep === 'complete' && ( -
- Gerando resposta... +
+

Como posso ajudar você hoje?

+

Selecione um modelo e comece uma conversa.

+
+
+ ) : ( +
+ {messages.map((msg, i) => { + const isLastMessage = i === messages.length - (isLoading ? 1 : 0); // Ajuste simples + // Streaming logica mantida simples + const isAssistantStreaming = i === messages.length - 1 && msg.role === 'assistant' && isLoading; + + return ( +
+ +
+ ); + })} + + {isLoading && ( +
+
+ Pensando... +
+ )}
)} -
- )} + {/* Elemento invisível para scroll automático */}
- - {/* Thinking Indicator - aparece apenas quando não há web search ativo */} - {thinkingStep && thinkingStep !== 'complete' && (webSearch.status === 'idle') && ( - - )} - - {/* Raw Context Debug */} - {lastContextSources.length > 0 && ( -
- - {showContextDebug && ( -
-
-

- Fontes utilizadas ({lastContextSources.length}): -

-
- {lastContextSources.map((source, idx) => ( - - {source.title || source.url} - - ))} + + {/* Debug Info (Moved inside scroll) */} + {(deepResearch.state.logs.length > 0 || lastWebContext) && ( +
+
+ {/* Process Log Toggle */} + {deepResearch.state.logs.length > 0 && ( +
+ + {showProcessDebug && ( +
+
+ +
+ {deepResearch.state.logs.map((log, idx) => ( +
+ {log.stage} + {log.input ? (log.input.length > 50 ? log.input.substring(0, 50) + '...' : log.input) : '-'} +
+ ))} +
+ )} +
+ )} + + {/* Context Debug Toggle */} + {lastWebContext && ( +
+ + {showContextDebug && ( +
+
+ {lastContextSources.map((s, i) => ( + {s.title || s.url} + ))} +
+
+

Preview Contexto:

+
{lastWebContext.substring(0, 500)}...
+
+
+ )} +
+ )}
-
-

- Bloco enviado na mensagem do usuário: -

-
-                        {lastWebContext.trim().length > 0 ? lastWebContext : '[Contexto vazio após sanitização]'}
-                      
-
-
- )} + )} + + {/* Spacer for Bottom Input */} +
+
- )} - {/* Input */} - { - const cat = settings.webSearch.categories.find(c => c.id === id); - if (!cat) return; - settings.updateCategory({ ...cat, enabled }); - }} - /> -
+ {/* 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.

+
+
+
+
- + + { + 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); + }} + />
); } + diff --git a/app/page.tsx b/app/page.tsx index 5acb93f..1d66491 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,13 +4,36 @@ import { useOllamaCheck } from "@/hooks/use-ollama-check"; import { Hero } from "@/components/landing/hero"; import { InstallModal } from "@/components/modals/install-modal"; import { StoppedCard } from "@/components/landing/stopped-card"; +import { WelcomeNotification } from "@/components/notifications/welcome-notification"; import { Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function Home() { const { status, check } = useOllamaCheck(); const router = useRouter(); + // Tentar iniciar Ollama automaticamente quando o componente montar + useEffect(() => { + const tryAutoStart = async () => { + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke('auto_start_ollama'); + // Aguardar um pouco e verificar novamente + setTimeout(() => { + check(); + }, 2000); + } catch (error) { + console.error('Erro ao tentar iniciar Ollama automaticamente:', error); + } + }; + + // Só tentar se estiver instalado mas não rodando + if (status === 'installed_stopped') { + tryAutoStart(); + } + }, [status, check]); + const handleStart = () => { const setupComplete = localStorage.getItem("ollahub_setup_complete"); if (setupComplete) { @@ -31,6 +54,8 @@ export default function Home() { return (
+ {/* Notificação de boas-vindas */} + {/* Layout for Running State */} {status === 'running' && ( diff --git a/app/setup/page.tsx b/app/setup/page.tsx index 839cd15..718ff92 100644 --- a/app/setup/page.tsx +++ b/app/setup/page.tsx @@ -2,12 +2,13 @@ import { useHardware } from "@/hooks/use-hardware"; import { HardwareScan } from "@/components/setup/hardware-scan"; +import { GpuSelector } from "@/components/setup/gpu-selector"; import { ModelSelector } from "@/components/setup/model-selector"; import { getRecommendation } from "@/lib/recommendation"; import { useRouter } from "next/navigation"; export default function SetupPage() { - const { specs, loading } = useHardware(); + const { specs, loading, gpus } = useHardware(); const router = useRouter(); const handleComplete = () => { @@ -26,6 +27,12 @@ export default function SetupPage() { + {!loading && specs && gpus.length > 0 && ( +
+ +
+ )} + {!loading && specs && (
void; @@ -31,6 +49,35 @@ export function ChatInput({ const [input, setInput] = useState(""); const textareaRef = useRef(null); const [webDialogOpen, setWebDialogOpen] = useState(false); + const settings = useSettingsStore(); + + // Validação em tempo real + const validation = useMemo(() => { + if (!input.trim()) { + return { isValid: true, errors: [], warnings: [] }; + } + const preprocessingConfig = settings.queryPreprocessing || { + enabled: true, + minLength: 3, + maxLength: 2000, + autoSplitQuestions: true, + irrelevantPatterns: [], + }; + + if (!preprocessingConfig.enabled) { + return { isValid: true, errors: [], warnings: [] }; + } + + return validateQuery(input, { + minLength: preprocessingConfig.minLength, + maxLength: preprocessingConfig.maxLength, + irrelevantPatterns: preprocessingConfig.irrelevantPatterns.length > 0 + ? preprocessingConfig.irrelevantPatterns + : undefined, + }); + }, [input, settings.queryPreprocessing]); + + const isValid = validation.isValid; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -40,7 +87,7 @@ export function ChatInput({ }; const handleSend = () => { - if (input.trim() && !isLoading) { + if (input.trim() && !isLoading && isValid) { onSend(input); setInput(""); } @@ -49,31 +96,37 @@ export function ChatInput({ useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + const scrollHeight = textareaRef.current.scrollHeight; + // Limitar altura máxima a uns 200px + textareaRef.current.style.height = `${Math.min(scrollHeight, 200)}px`; } }, [input]); return ( -
-
-
+
+
+