From ad2ce1e8874302619c3ea9d898f35418f3b1a89a Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Thu, 27 Nov 2025 18:16:20 -0300 Subject: [PATCH] feat: enhance chat and settings UI with responsive design and new components - Updated global styles to improve layout and prevent overflow in chat and settings pages. - Introduced new sidebar chat item styles for better truncation and overflow handling. - Enhanced settings page layout for improved responsiveness and user experience. - Added icons to category selection in chat input for better visual representation. - Refactored task management UI for improved accessibility and usability. --- app/chat/page.tsx | 61 ++++++++---------- app/globals.css | 50 ++++++++++++++- app/settings/page.tsx | 97 +++++++++++++++++----------- app/tasks/page.tsx | 105 ++++++++++++++++++++----------- components/chat/chat-input.tsx | 37 ++++++++--- components/chat/sidebar-list.tsx | 26 ++++---- 6 files changed, 242 insertions(+), 134 deletions(-) diff --git a/app/chat/page.tsx b/app/chat/page.tsx index c1e5cba..430cf3c 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -3,9 +3,7 @@ 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, Plus, ScrollText, Copy, Check, Globe } from "lucide-react"; +import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, ScrollText, Copy, Check, Globe, RefreshCw, Cpu } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { ChatInput } from "@/components/chat/chat-input"; import { ChatMessage } from "@/components/chat/chat-message"; @@ -25,18 +23,16 @@ 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, useMemo } from "react"; +import { useState, useEffect, useRef, useCallback } 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 { executeProgressiveSearch } from "@/lib/web-search-fallback"; +// @ts-expect-error - MD file import import defaultFormatPrompt from "@/data/prompts/default-format.md"; @@ -51,7 +47,6 @@ export default function ChatPage() { currentSessionId, setCurrentSessionId, loadSessionHistory, - loadSessions, saveSession, deleteSession, isGeneratingTitle, @@ -61,7 +56,7 @@ export default function ChatPage() { const { isDownloading, progress } = useAutoLabelingModel(); const webSearch = useWebSearch(); - const { generateQuery, isGenerating: isGeneratingQuery } = useQueryGenerator(); + const { generateQuery } = useQueryGenerator(); const deepResearch = useDeepResearch(); const settings = useSettingsStore(); const { preprocess } = useQueryPreprocessor(); @@ -91,7 +86,7 @@ export default function ChatPage() { setMessages(prev => [...prev, thinkingMessage]); return messageId; - }, []); + }, [setMessages]); const updateThinkingMessage = useCallback(( stepType: ThinkingStepType, @@ -110,23 +105,16 @@ export default function ChatPage() { } return msg; })); - }, []); + }, [setMessages]); - 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); const initializedRef = useRef(false); // Initialize with default format prompt - const [systemPrompt, setSystemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo."); + 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 [error, setError] = useState(null); + const [, setError] = useState(null); const chatsSidebarRef = useRef(null); const saveTimeoutRef = useRef(null); @@ -157,7 +145,7 @@ export default function ChatPage() { } } initializedRef.current = true; - }, [settings.selectedModel, models]); + }, [settings, models]); // Auto-scroll para o final durante streaming useEffect(() => { @@ -230,7 +218,7 @@ export default function ChatPage() { // Se inválida, retornar erro if (!preprocessed.validation.isValid) { - chatLog.error('Query validation failed:', preprocessed.validation.errors); + chatLog.error(`Query validation failed: ${preprocessed.validation.errors.join(', ')}`); throw new Error(`Query validation failed: ${preprocessed.validation.errors.join(', ')}`); } @@ -255,7 +243,7 @@ export default function ChatPage() { }; return { preprocessed, finalContent: content }; } - }, [settings.queryPreprocessing, webSearch.isEnabled]); + }, [settings.queryPreprocessing, webSearch.isEnabled, preprocess]); const handleSend = async (content: string) => { if (!selectedModel) return; @@ -297,19 +285,20 @@ export default function ChatPage() { }); // Atualizar step com erro - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMsg = error instanceof Error ? error.message : String(error); updateThinkingMessage('preprocessing', { status: 'error', - error: errorMessage, + error: errorMsg, duration: Date.now() - preprocessingStart, }); // Removido: setProcessSteps - agora usando mensagens thinking agrupadas // Definir erro global - setError(error); + const errorObj = error instanceof Error ? error : new Error(String(error)); + setError(errorObj); - chatLog.error('Preprocessing failed:', error); + chatLog.error(`Preprocessing failed: ${errorObj.message}`); // Erro já será mostrado no chat-input, apenas retornar return; } @@ -401,7 +390,7 @@ export default function ChatPage() { query, async (q, limit, round = 1) => { // Passar round para timeout adaptativo - return await webSearch.smartSearchRag(q, limit, undefined, undefined, round); + return await webSearch.smartSearchRag(q, limit, round); }, selectedModel, { @@ -477,6 +466,7 @@ export default function ChatPage() { scrapedSources = deepResearch.state.knowledgeBase.map(entry => ({ url: entry.sourceUrl, title: entry.title, + content: entry.content, markdown: entry.content, snippet: entry.content.substring(0, 200) })); @@ -532,7 +522,7 @@ export default function ChatPage() { const validation = validateCondensedContext(optimizedResult.result, content); if (!validation.isValid) { - chatLog.error('Context validation failed:', validation.warnings.join(', ')); + chatLog.error(`Context validation failed: ${validation.warnings.join(', ')}`); } else if (validation.warnings.length > 0) { validation.warnings.forEach(warning => { chatLog.warn(`⚠️ ${warning}`); @@ -614,7 +604,6 @@ export default function ChatPage() { // ========== LÓGICA DE DEEP RESEARCH (Knowledge Base Aggregation) ========== // Removido: setThinkingStep e setProcessSteps - agora usando mensagens thinking agrupadas - const researchStart = Date.now(); // Executar pesquisa web const { knowledgeBaseContext, scrapedSources, validationReport } = await executeWebResearch( content, @@ -885,7 +874,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente }; return ( -
+
{/* Left Sidebar (Nav) */} @@ -1022,8 +1011,9 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente - - {m.name} + + + {m.name} {m.size} @@ -1047,7 +1037,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
@@ -1117,7 +1107,6 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente } // Adicionar mensagem regular - const isLastMessage = i === messages.length - (isLoading ? 1 : 0); const isAssistantStreaming = i === messages.length - 1 && msg.role === 'assistant' && isLoading; groupedMessages.push({ diff --git a/app/globals.css b/app/globals.css index fc84304..c200a2f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -145,11 +145,22 @@ } @layer base { - * { + *, + *::before, + *::after { @apply border-border outline-ring/50; + box-sizing: border-box; } + + html { + overflow-x: hidden; + } + body { @apply bg-background text-foreground; + overflow-x: hidden; + width: 100%; + max-width: 100vw; } } @@ -226,6 +237,43 @@ padding: clamp(8px, 2vw, 16px); } +/* Sidebar chat item - prevent overflow and ensure proper truncation */ +.sidebar-chat-item { + width: 100%; + min-width: 0; + overflow: hidden; +} + +.sidebar-chat-item-content { + display: flex; + align-items: flex-start; + gap: 0.625rem; + width: 100%; + min-width: 0; +} + +.sidebar-chat-item-text { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.sidebar-chat-item-title { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +} + +.sidebar-chat-item-date { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +} + /* Responsive font sizes */ @media (max-width: 640px) { .responsive-text { diff --git a/app/settings/page.tsx b/app/settings/page.tsx index ffbbbe7..97e8f3f 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -217,32 +217,47 @@ export default function SettingsPage() { }; return ( -
-
-
-

Configurações

-

- Gerencie as preferências do OllaHub -

+
+
+
+
+

Configurações

+

+ Gerencie as preferências do OllaHub +

+
+
-
- - - Visão Geral - General - Web Search - Sources & Knowledge - Tasks & Scheduler - System & Logs + + + + Visão Geral + + + General + + + Web Search + + + Sources + + + Tasks + + + System + {/* Tab: Visão Geral (Hardware Dashboard) */} @@ -272,17 +287,19 @@ export default function SettingsPage() {
-
+
settings.setOllamaUrl(e.target.value)} placeholder="http://localhost:11434" + className="flex-1 min-w-0" /> @@ -855,7 +874,7 @@ export default function SettingsPage() { {/* Edit Category Dialog */} !open && setEditingCategory(null)}> - + Editar Categoria: {editingCategory?.name} @@ -1077,18 +1096,19 @@ export default function SettingsPage() { {/* Clear History Dialog */} - + Confirmar Exclusão Esta ação não pode ser desfeita. Todos os chats serão permanentemente removidos. - + @@ -1096,6 +1116,7 @@ export default function SettingsPage() { variant="destructive" onClick={handleClearHistory} disabled={isClearing} + className="w-full sm:w-auto" > {isClearing ? ( <> diff --git a/app/tasks/page.tsx b/app/tasks/page.tsx index 894e3fe..6e22122 100644 --- a/app/tasks/page.tsx +++ b/app/tasks/page.tsx @@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; -import { Plus, Trash2, Clock, Play, Pause, CheckCircle2, XCircle } from 'lucide-react'; +import { Plus, Trash2, Clock, Play, Pause, CheckCircle2, XCircle, X, ArrowLeft } from 'lucide-react'; +import { useRouter } from 'next/navigation'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; @@ -32,6 +33,7 @@ interface Task { } export default function TasksPage() { + const router = useRouter(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -100,36 +102,25 @@ export default function TasksPage() { }; return ( -
-
-
-

Tarefas Agendadas

-

- Gerencie tarefas que executam automaticamente em segundo plano -

+
+
+
+
+

Tarefas Agendadas

+

+ Gerencie tarefas que executam automaticamente em segundo plano +

+
+
- - - - - - - Criar Nova Task - - Configure uma tarefa para executar automaticamente - - - { - setIsDialogOpen(false); - loadTasks(); - }} - /> - -
{loading ? ( @@ -139,16 +130,59 @@ export default function TasksPage() { ) : tasks.length === 0 ? ( -

+

Nenhuma task agendada ainda.

- + + + + + + + Criar Nova Task + + Configure uma tarefa para executar automaticamente + + + { + setIsDialogOpen(false); + loadTasks(); + }} + /> + +
) : ( + <> +
+ + + + + + + Criar Nova Task + + Configure uma tarefa para executar automaticamente + + + { + setIsDialogOpen(false); + loadTasks(); + }} + /> + + +
{tasks.map(task => ( @@ -218,6 +252,7 @@ export default function TasksPage() { ))}
+ )}
); diff --git a/components/chat/chat-input.tsx b/components/chat/chat-input.tsx index 024ad07..1a30786 100644 --- a/components/chat/chat-input.tsx +++ b/components/chat/chat-input.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Send, Square, Globe, WifiOff, ChevronUp, AlertCircle } from "lucide-react"; +import { Send, Square, Globe, WifiOff, ChevronUp, AlertCircle, GraduationCap, Newspaper, Terminal, TrendingUp } from "lucide-react"; import { useState, useRef, useEffect, useMemo } from "react"; import { Tooltip, @@ -169,15 +169,32 @@ export function ChatInput({ Fontes de conhecimento - {categories.map((cat) => ( - onToggleCategory(cat.id, !!checked)} - > - {cat.name} - - ))} + {categories.map((cat) => { + // Mapeamento de ícones por categoria + const getCategoryIcon = (categoryId: string) => { + const iconMap: Record = { + 'academic': , + 'news': , + 'tech': , + 'finance': , + }; + return iconMap[categoryId] || null; + }; + + return ( + onToggleCategory(cat.id, !!checked)} + className="pl-8" + > +
+ {getCategoryIcon(cat.id)} + {cat.name} +
+
+ ); + })}
)} diff --git a/components/chat/sidebar-list.tsx b/components/chat/sidebar-list.tsx index 96158c5..6f7b685 100644 --- a/components/chat/sidebar-list.tsx +++ b/components/chat/sidebar-list.tsx @@ -102,9 +102,9 @@ export function SidebarList({ return ( -
+
{/* Header com busca e botão Nova Conversa */} -
+
{/* Campo de busca */}
@@ -132,7 +132,7 @@ export function SidebarList({ {/* Botão Nova Conversa */}