'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 { 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 { ChatInput } from "@/components/chat/chat-input"; import { ChatMessage } from "@/components/chat/chat-message"; import { SidebarList } from "@/components/chat/sidebar-list"; import { useChat } from "@/hooks/use-chat"; 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"; import { useQueryGenerator } from "@/hooks/use-query-generator"; 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 { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; import { ImperativePanelHandle } from "react-resizable-panels"; import { sanitizeWebSources } from "@/lib/sanitize-web-content"; // @ts-ignore 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 { sessions, currentSessionId, setCurrentSessionId, loadSessionHistory, loadSessions, saveSession, deleteSession, isGeneratingTitle } = useChatStorage(); const { isDownloading, progress } = useAutoLabelingModel(); const webSearch = useWebSearch(); const { generateQuery, isGenerating: isGeneratingQuery } = useQueryGenerator(); const settings = useSettingsStore(); const [selectedModel, setSelectedModel] = useState(""); const [mounted, setMounted] = useState(false); const initializedRef = useRef(false); // Initialize with default format prompt const [systemPrompt, setSystemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo."); const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(false); const [thinkingStep, setThinkingStep] = useState(null); const chatsSidebarRef = useRef(null); const saveTimeoutRef = useRef(null); const lastSavedMessagesRef = useRef(0); const messagesEndRef = useRef(null); const [showContextDebug, setShowContextDebug] = useState(false); const [lastWebContext, setLastWebContext] = useState(''); const [lastContextSources, setLastContextSources] = useState([]); // Auto-select first model useEffect(() => { setMounted(true); }, []); useEffect(() => { if (initializedRef.current) return; const stored = settings.selectedModel; const first = models.length > 0 ? models[0].name : ""; const next = stored || first || ""; if (next) { setSelectedModel(next); if (!stored) { settings.setSelectedModel(next); } } initializedRef.current = true; }, [settings.selectedModel, models]); // 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]); const handleSend = async (content: string) => { if (!selectedModel) return; setShowContextDebug(false); setLastWebContext(''); setLastContextSources([]); // 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) } // ========== LÓGICA DE AGENTE DE PESQUISA ========== let webContext = ''; let searchQuery = ''; let scrapedSources: ScrapedContent[] = []; // Iniciar indicador de pensamento setThinkingStep('analyzing'); if (webSearch.isEnabled) { try { webSearch.reset(); // Resetar estado anterior // 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); } } else { setLastWebContext(''); setLastContextSources([]); } } catch (error) { console.error('Erro na pesquisa web:', error); // Continuar mesmo se a pesquisa falhar } } // Passo 3: Formular resposta setThinkingStep('formulating'); // ========== MONTAR SYSTEM PROMPT COM CONTEXTO TEMPORAL ========== // 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; // Adicionar contexto temporal e web if (webContext) { 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 ${webContext} --- `; } 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. --- `; } // [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] let finalUserContent = content; if (webContext) { finalUserContent = `[CONTEXTO WEB OBRIGATÓRIO]\n${webContext}\n[/CONTEXTO WEB]\n\nCom base EXCLUSIVAMENTE no texto acima, responda: ${content}`; } // Enviar o conteúdo original para UI, mas com override no payload para incluir contexto await sendMessage(content, selectedModel, enhancedSystemPrompt, { payloadContentOverride: finalUserContent, }); // 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; }); } // Marcar como completo quando começar a streamar setTimeout(() => setThinkingStep('complete'), 100); }; 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; }; 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) { panel.expand(); } else { panel.collapse(); } } }; return (
{/* Left Sidebar (Nav) */}
{/* Sessions List */} setIsChatsSidebarCollapsed(true)} onExpand={() => setIsChatsSidebarCollapsed(false)} className="min-w-[200px]" > {/* Main Chat Area */}
{/* Header */}
{isChatsSidebarCollapsed ? ( ) : ( )}
Chat {isGeneratingTitle && } {isDownloading && ( {progress} )}
{/* 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:

)}
); })} {isLoading && thinkingStep === 'complete' && (
Gerando resposta...
)}
)} {/* 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} ))}

Bloco enviado na mensagem do usuário:

                        {lastWebContext.trim().length > 0 ? lastWebContext : '[Contexto vazio após sanitização]'}
                      
)}
)} {/* Input */} { const cat = settings.webSearch.categories.find(c => c.id === id); if (!cat) return; settings.updateCategory({ ...cat, enabled }); }} />
); }