From 8834ef2b4fcab2f9af671d603df8167d5cec00a8 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Thu, 20 Nov 2025 04:26:35 -0300 Subject: [PATCH] feat: update dependencies and enhance chat functionality - Added new Radix UI components including checkbox, label, popover, slider, switch, and tooltip to improve UI interactions. - Integrated react-syntax-highlighter for better code rendering in chat messages. - Updated package.json and pnpm-lock.yaml to include new dependencies and ensure compatibility. - Enhanced chat input to support web search toggling, improving user experience during message composition. - Implemented auto-saving of chat sessions and improved session management features. - Refactored chat message rendering to clean up metadata and improve display quality. --- IMPLEMENTATION_GUIDE.md | 309 + app/chat/page.tsx | 440 +- app/settings/page.tsx | 638 ++ app/tasks/page.tsx | 457 + components/chat/chat-input.tsx | 113 +- components/chat/chat-message.tsx | 25 +- components/chat/code-block.tsx | 2 + components/chat/markdown-renderers.tsx | 2 + components/chat/mcp-config-panel.tsx | 419 + components/chat/mcp-json-editor.tsx | 184 + components/chat/mcp-server-selector.tsx | 127 + components/chat/message-actions.tsx | 2 + components/chat/search-progress.tsx | 152 + components/chat/sidebar-list.tsx | 89 + components/chat/system-panel.tsx | 2 + components/chat/thinking-indicator.tsx | 105 + components/chat/web-search-status.tsx | 164 + components/settings/domain-tags-input.tsx | 73 + components/theme-provider.tsx | 2 + components/ui/dropdown-menu.tsx | 2 + components/ui/input.tsx | 26 + components/ui/label.tsx | 27 + components/ui/progress.tsx | 2 + components/ui/select.tsx | 2 + components/ui/slider.tsx | 27 + components/ui/switch.tsx | 30 + components/ui/textarea.tsx | 2 + components/ui/tooltip.tsx | 29 + data/prompts/default-format.md | 67 +- hooks/use-auto-labeling-model.ts | 59 + hooks/use-chat-storage.ts | 381 + hooks/use-chat.ts | 322 +- hooks/use-local-models.ts | 2 + hooks/use-mcp-servers.ts | 73 + hooks/use-mcp-tools.ts | 133 + hooks/use-prompt-generator.ts | 2 + hooks/use-query-generator.ts | 91 + hooks/use-system-monitor.ts | 2 + hooks/use-toast.ts | 27 + hooks/use-web-search.ts | 161 + lib/metadata.ts | 23 + lib/sanitize-web-content.ts | 122 + package.json | 11 +- pnpm-lock.yaml | 329 + repomix-output.xml | 10168 ++++++++++++++++++++ services/webSearch.ts | 330 + src-tauri/Cargo.lock | 1591 ++- src-tauri/Cargo.toml | 19 +- src-tauri/src/lib.rs | 1288 ++- src-tauri/src/ollama_client.rs | 153 + src-tauri/src/scheduler.rs | 163 + src-tauri/src/scheduler_loop.rs | 128 + src-tauri/src/task_executor.rs | 276 + src-tauri/src/web_scraper.rs | 574 ++ store/settings-store.ts | 242 + 55 files changed, 20030 insertions(+), 159 deletions(-) create mode 100644 IMPLEMENTATION_GUIDE.md create mode 100644 app/settings/page.tsx create mode 100644 app/tasks/page.tsx create mode 100644 components/chat/mcp-config-panel.tsx create mode 100644 components/chat/mcp-json-editor.tsx create mode 100644 components/chat/mcp-server-selector.tsx create mode 100644 components/chat/search-progress.tsx create mode 100644 components/chat/sidebar-list.tsx create mode 100644 components/chat/thinking-indicator.tsx create mode 100644 components/chat/web-search-status.tsx create mode 100644 components/settings/domain-tags-input.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-auto-labeling-model.ts create mode 100644 hooks/use-chat-storage.ts create mode 100644 hooks/use-mcp-servers.ts create mode 100644 hooks/use-mcp-tools.ts create mode 100644 hooks/use-query-generator.ts create mode 100644 hooks/use-toast.ts create mode 100644 hooks/use-web-search.ts create mode 100644 lib/metadata.ts create mode 100644 lib/sanitize-web-content.ts create mode 100644 repomix-output.xml create mode 100644 services/webSearch.ts create mode 100644 src-tauri/src/ollama_client.rs create mode 100644 src-tauri/src/scheduler.rs create mode 100644 src-tauri/src/scheduler_loop.rs create mode 100644 src-tauri/src/task_executor.rs create mode 100644 src-tauri/src/web_scraper.rs create mode 100644 store/settings-store.ts diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..a37f609 --- /dev/null +++ b/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,309 @@ +# Guia de Implementação: System Tray, Background Mode e Task Scheduler + +## Status da Implementação + +### ✅ Concluído + +1. **Estrutura de Tasks** (`src-tauri/src/scheduler.rs`) + - `SentinelTask` com suporte a múltiplos tipos de ações + - Persistência em `tasks.json` + - Gerenciamento de estado thread-safe + +2. **Cliente Ollama Headless** (`src-tauri/src/ollama_client.rs`) + - Comunicação direta com Ollama via `reqwest` + - Suporte a streaming + - Verificação de conexão + +3. **Executor de Tasks** (`src-tauri/src/task_executor.rs`) + - Execução de `SearchAndSummarize` + - Execução de `JustPing` + - Execução de `CustomPrompt` + - Integração com file locks para escrita segura + +4. **Scheduler Loop** (`src-tauri/src/scheduler_loop.rs`) + - Integração com `tokio-cron-scheduler` + - Recarregamento dinâmico de tasks + - Execução assíncrona em background + +### 🔨 Pendente (Próximos Passos) + +#### 1. Comandos Tauri para CRUD de Tasks + +Adicione ao `src-tauri/src/lib.rs`: + +```rust +use scheduler::{SchedulerService, SchedulerState, SentinelTask, TaskAction}; +use chrono::Utc; +use uuid::Uuid; + +#[command] +async fn create_task( + scheduler: State<'_, SchedulerState>, + label: String, + cron_schedule: String, + action: TaskAction, +) -> Result { + let task = SentinelTask { + id: Uuid::new_v4().to_string(), + label, + cron_schedule, + action, + enabled: true, + last_run: None, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + let mut sched = scheduler.lock().await; + sched.upsert_task(task.clone())?; + Ok(task.id) +} + +#[command] +async fn list_tasks( + scheduler: State<'_, SchedulerState>, +) -> Result, String> { + let sched = scheduler.lock().await; + Ok(sched.list_tasks()) +} + +#[command] +async fn update_task( + scheduler: State<'_, SchedulerState>, + task: SentinelTask, +) -> Result<(), String> { + let mut sched = scheduler.lock().await; + let mut updated = task; + updated.updated_at = Utc::now(); + sched.upsert_task(updated) +} + +#[command] +async fn delete_task( + scheduler: State<'_, SchedulerState>, + id: String, +) -> Result<(), String> { + let mut sched = scheduler.lock().await; + sched.remove_task(&id) +} + +#[command] +async fn toggle_task( + scheduler: State<'_, SchedulerState>, + id: String, + enabled: bool, +) -> Result<(), String> { + let mut sched = scheduler.lock().await; + if let Some(mut task) = sched.get_task(&id).cloned() { + task.enabled = enabled; + task.updated_at = Utc::now(); + sched.upsert_task(task) + } else { + Err("Task not found".to_string()) + } +} +``` + +#### 2. System Tray Setup + +No `src-tauri/src/lib.rs`, modifique o `.setup()`: + +```rust +use tauri::{Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem}; + +.setup(|app| { + // ... código existente ... + + // Criar System Tray + let tray_menu = SystemTrayMenu::new() + .add_item(SystemTrayMenuItem::with_id("show", "Mostrar OllaHub", true, None)) + .add_item(SystemTrayMenuItem::with_id("pause", "Pausar Tasks", true, None)) + .add_native_item(SystemTrayMenuItem::Separator) + .add_item(SystemTrayMenuItem::with_id("quit", "Sair", true, None)); + + let system_tray = SystemTray::new().with_menu(tray_menu); + + app.handle().system_tray(system_tray)?; + + // Handler de eventos do Tray + app.handle().on_system_tray_event(|app, event| { + if let SystemTrayEvent::MenuItemClick { id, .. } = event { + match id.as_str() { + "show" => { + if let Some(window) = app.get_window("main") { + window.show().unwrap(); + window.set_focus().unwrap(); + } + } + "pause" => { + // Implementar lógica de pausar tasks + log::info!("Pausar tasks - implementar"); + } + "quit" => { + app.exit(0); + } + _ => {} + } + } + }); + + // Modificar comportamento de fechar janela + if let Some(window) = app.get_window("main") { + window.on_window_event(|event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Ocultar ao invés de fechar + if let Some(window) = event.window() { + window.hide().unwrap(); + } + api.prevent_close(); + } + }); + } + + Ok(()) +}) +``` + +#### 3. Inicializar Scheduler no Setup + +No mesmo `.setup()`, após criar o scheduler: + +```rust +// Inicializar scheduler +let scheduler_service = SchedulerService::new(app.handle().clone())?; +let scheduler_state: SchedulerState = Arc::new(Mutex::new(scheduler_service)); +let browser_state: BrowserState = app.state(); + +// Iniciar loop do scheduler em background +let app_handle = app.handle().clone(); +let scheduler_clone = scheduler_state.clone(); +let browser_clone = browser_state.clone(); +tokio::spawn(async move { + if let Err(e) = scheduler_loop::start_scheduler_loop( + app_handle, + scheduler_clone, + browser_clone, + None, // Ollama URL - pode vir do settings + ).await { + log::error!("Erro ao iniciar scheduler: {}", e); + } +}); + +// Adicionar ao manage +.manage(scheduler_state) +``` + +#### 4. Adicionar Comandos ao Invoke Handler + +```rust +.invoke_handler(tauri::generate_handler![ + // ... comandos existentes ... + create_task, + list_tasks, + update_task, + delete_task, + toggle_task, +]) +``` + +#### 5. Interface React (app/tasks/page.tsx) + +Crie uma página básica para gerenciar tasks: + +```tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +interface Task { + id: string; + label: string; + cron_schedule: string; + action: any; + enabled: boolean; + last_run: string | null; +} + +export default function TasksPage() { + const [tasks, setTasks] = useState([]); + + useEffect(() => { + loadTasks(); + }, []); + + const loadTasks = async () => { + try { + const loaded = await invoke('list_tasks'); + setTasks(loaded); + } catch (error) { + console.error('Erro ao carregar tasks:', error); + } + }; + + const toggleTask = async (id: string, enabled: boolean) => { + try { + await invoke('toggle_task', { id, enabled: !enabled }); + await loadTasks(); + } catch (error) { + console.error('Erro ao alternar task:', error); + } + }; + + return ( +
+

Tarefas Agendadas

+
+ {tasks.map(task => ( + + + {task.label} + + +

Cron: {task.cron_schedule}

+

Status: {task.enabled ? 'Ativa' : 'Pausada'}

+ +
+
+ ))} +
+
+ ); +} +``` + +## Dependências Adicionais Necessárias + +No `Cargo.toml`, já foram adicionadas: +- `tokio-cron-scheduler = "0.9"` +- `futures-util = "0.3"` +- `tauri-plugin-notification = "2"` + +**Nota para Linux:** Para System Tray funcionar, pode ser necessário: +```bash +# Ubuntu/Debian +sudo apt-get install libappindicator3-1 + +# Arch/Manjaro +sudo pacman -S libappindicator-gtk3 +``` + +## Próximos Passos + +1. Compilar e testar: `cd src-tauri && cargo build` +2. Corrigir erros de compilação +3. Testar criação de task via frontend +4. Verificar se scheduler executa corretamente +5. Testar System Tray em diferentes plataformas + +## Notas Importantes + +- **File Locking**: Já implementado - tasks usam o mesmo sistema de locks que salvar sessões +- **Concorrência**: Scheduler roda em thread separada, não bloqueia UI +- **Persistência**: Tasks são salvas em `app_data_dir/tasks.json` +- **Notificações**: Usa `tauri-plugin-notification` para notificações nativas + diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 50cbfe3..ab98f4f 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -5,30 +5,63 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2 } from "lucide-react"; import { ChatInput } from "@/components/chat/chat-input"; import { ChatMessage } from "@/components/chat/chat-message"; import { SystemPanel } from "@/components/chat/system-panel"; import { PromptGeneratorDialog } from "@/components/chat/prompt-generator"; +import { SidebarList } from "@/components/chat/sidebar-list"; import { useChat } from "@/hooks/use-chat"; import { useLocalModels } from "@/hooks/use-local-models"; -import { useState, useEffect, useRef } from "react"; +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 { invoke } from "@tauri-apps/api/core"; +import { useState, useEffect, useRef, useCallback } from "react"; import { useTheme } from "next-themes"; 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 { messages, sendMessage, isLoading, stop } = useChat(); + const { messages, setMessages, sendMessage, isLoading, stop, clearChat } = useChat(); const { models } = 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(""); // Initialize with default format prompt const [systemPrompt, setSystemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo."); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(false); + const [thinkingStep, setThinkingStep] = useState(null); - const sidebarRef = useRef(null); + const chatsSidebarRef = useRef(null); + const saveTimeoutRef = useRef(null); + const lastSavedMessagesRef = useRef(0); + const messagesEndRef = useRef(null); // Auto-select first model useEffect(() => { @@ -37,15 +70,226 @@ export default function ChatPage() { } }, [models, selectedModel]); - const handleSend = (content: string) => { + // 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; - sendMessage(content, selectedModel, systemPrompt); + + // 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: any[] = []; + + // 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'); + // Buscar fontes usando configurações do store + const maxResults = settings.webSearch.maxResults; + const excludedDomains = settings.webSearch.excludedDomains; + scrapedSources = await webSearch.search(searchQuery, maxResults, excludedDomains); + + // 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, + }); + + // Log para debug: verificar tamanho do contexto gerado + const contextLength = webContext.length; + console.debug(`Contexto web gerado (sanitizado): ${contextLength} caracteres de ${scrapedSources.length} fontes`); + } + } + } 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"). + - Cite as fontes usando [1], [2], [3] ao final das frases quando usar informações do contexto web. + - 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. + +--- +`; + } + + await sendMessage(content, selectedModel, enhancedSystemPrompt); + + // Marcar como completo quando começar a streamar + setTimeout(() => setThinkingStep('complete'), 100); }; - const toggleSidebar = () => { - const panel = sidebarRef.current; + 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 (isSidebarCollapsed) { + if (isChatsSidebarCollapsed) { panel.expand(); } else { panel.collapse(); @@ -59,15 +303,10 @@ export default function ChatPage() { setIsSidebarCollapsed(true)} - onExpand={() => setIsSidebarCollapsed(false)} - className="border-r flex flex-col items-center py-4 gap-4 bg-muted/20 min-w-[60px]" + maxSize={4} + className="border-r flex flex-col items-center py-4 gap-4 bg-muted/20 w-[60px]" > - )} - {!isSidebarCollapsed && ( - )} -
Chat
+
+ Chat + {isGeneratingTitle && } + {isDownloading && ( + + + {progress} + + )} +
@@ -134,24 +404,93 @@ export default function ChatPage() {

Inicie uma conversa com {selectedModel}

) : ( -
- {messages.map((msg, i) => ( - - ))} - {isLoading && ( +
+ {messages.map((msg, i) => { + // Verificar se a mensagem anterior teve busca web + const hasWebSources = i > 0 && + messages[i - 1].role === 'user' && + webSearch.scrapedSources.length > 0 && + webSearch.status === 'completed'; + + return ( +
+ + {/* Mostrar fontes após resposta do assistente que usou web search */} + {msg.role === 'assistant' && hasWebSources && ( +
+
+

+ Fontes consultadas: +

+ +
+
+ )} +
+ ); + })} + {/* Web Search Progress - mostrar antes da resposta da IA */} + {(webSearch.status === 'searching' || + webSearch.status === 'scraping' || + webSearch.status === 'completed' || + webSearch.status === 'error') && ( + + )} + {isLoading && thinkingStep === 'complete' && (
Gerando resposta...
)}
)} + {/* Elemento invisível para scroll automático */} +
+ {/* Thinking Indicator */} + {thinkingStep && thinkingStep !== 'complete' && ( + + )} + {/* Input */}
@@ -159,8 +498,8 @@ export default function ChatPage() { {/* Right Sidebar (Settings & Monitor) */} - - + +
- -
-
- - +
+ {/* System Prompt */} +
+
+ + +
+