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.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-27 18:16:20 -03:00
1 parent 6a7eeaf7af
commit ad2ce1e887
6 files changed
+242 -134

No files matched your search

+25 -36
View File
@@ -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<string | Error | null>(null);
const [, setError] = useState<string | Error | null>(null);
const chatsSidebarRef = useRef<ImperativePanelHandle>(null);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(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 (
<div className="h-screen w-full bg-background overflow-hidden flex">
<div className="h-screen w-full bg-background overflow-hidden flex overflow-x-hidden">
{/* Left Sidebar (Nav) */}
<ResizablePanelGroup direction="horizontal" className="flex-1">
@@ -1022,8 +1011,9 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
<TooltipTrigger asChild>
<SelectItem value={m.name} className="min-w-0">
<span className="flex items-center justify-between w-full min-w-0 gap-2">
<span className="truncate flex-1 min-w-0" style={{ maxWidth: '70vw' }}>
{m.name}
<span className="flex items-center gap-2 truncate flex-1 min-w-0" style={{ maxWidth: '70vw' }}>
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
<span className="truncate">{m.name}</span>
</span>
<span className="ml-2 text-xs text-muted-foreground shrink-0">{m.size}</span>
</span>
@@ -1047,7 +1037,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</TooltipProvider>
</div>
<Button variant="outline" size="icon" className="h-9 w-9" onClick={refresh} title="Atualizar modelos">
<Loader2 className="w-4 h-4" />
<RefreshCw className="w-4 h-4" />
</Button>
</div>
</div>
@@ -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({
+49 -1
View File
@@ -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 {
+59 -38
View File
@@ -217,32 +217,47 @@ export default function SettingsPage() {
};
return (
<div className="container mx-auto py-8 max-w-4xl">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Configurações</h1>
<p className="text-muted-foreground mt-2">
Gerencie as preferências do OllaHub
</p>
<div className="container mx-auto py-4 sm:py-6 md:py-8 px-3 sm:px-4 md:px-6 max-w-4xl w-full overflow-x-hidden">
<div className="mb-4 sm:mb-6">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl font-bold truncate">Configurações</h1>
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">
Gerencie as preferências do OllaHub
</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => router.push('/chat')}
className="rounded-lg shrink-0"
title="Fechar configurações"
>
<X className="w-5 h-5" />
</Button>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => router.push('/chat')}
className="rounded-lg"
>
<X className="w-5 h-5" />
</Button>
</div>
<Tabs defaultValue="overview" className="space-y-6">
<TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="overview">Visão Geral</TabsTrigger>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="web">Web Search</TabsTrigger>
<TabsTrigger value="sources">Sources & Knowledge</TabsTrigger>
<TabsTrigger value="tasks">Tasks & Scheduler</TabsTrigger>
<TabsTrigger value="system">System & Logs</TabsTrigger>
<Tabs defaultValue="overview" className="space-y-4 sm:space-y-6">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-1 sm:gap-2 h-auto">
<TabsTrigger value="overview" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
Visão Geral
</TabsTrigger>
<TabsTrigger value="general" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
General
</TabsTrigger>
<TabsTrigger value="web" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
Web Search
</TabsTrigger>
<TabsTrigger value="sources" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
Sources
</TabsTrigger>
<TabsTrigger value="tasks" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
Tasks
</TabsTrigger>
<TabsTrigger value="system" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
System
</TabsTrigger>
</TabsList>
{/* Tab: Visão Geral (Hardware Dashboard) */}
@@ -272,17 +287,19 @@ export default function SettingsPage() {
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ollama-url">Ollama Endpoint</Label>
<div className="flex gap-2">
<div className="flex flex-col sm:flex-row gap-2">
<Input
id="ollama-url"
value={settings.ollamaUrl}
onChange={(e) => settings.setOllamaUrl(e.target.value)}
placeholder="http://localhost:11434"
className="flex-1 min-w-0"
/>
<Button
onClick={checkConnection}
disabled={isCheckingConnection}
variant="outline"
className="shrink-0"
>
{isCheckingConnection ? (
<Loader2 className="w-4 h-4 animate-spin" />
@@ -404,8 +421,8 @@ export default function SettingsPage() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-0">
<div className="space-y-0.5 flex-1 min-w-0">
<Label htmlFor="autostart">Iniciar com o Sistema</Label>
<p className="text-xs text-muted-foreground">
O aplicativo será iniciado automaticamente ao fazer login
@@ -415,6 +432,7 @@ export default function SettingsPage() {
id="autostart"
checked={settings.autoStart}
onCheckedChange={settings.toggleAutoStart}
className="shrink-0"
/>
</div>
</CardContent>
@@ -448,7 +466,7 @@ export default function SettingsPage() {
<div className="space-y-2">
<Label htmlFor="max-results">Máximo de Resultados</Label>
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 sm:gap-3">
<Input
id="max-results"
type="number"
@@ -458,12 +476,12 @@ export default function SettingsPage() {
const clamped = Math.min(100, Math.max(1, isNaN(v) ? 1 : v));
settings.setWebSearchMaxResults(clamped);
}}
className="w-24"
className="w-20 sm:w-24 shrink-0"
min={1}
max={100}
disabled={!settings.webSearch.enabled}
/>
<div className="flex-1">
<div className="flex-1 min-w-0">
<Slider
value={[settings.webSearch.maxResults]}
onValueChange={([value]) => settings.setWebSearchMaxResults(value)}
@@ -788,15 +806,15 @@ export default function SettingsPage() {
return (
<Card key={category.id} className={category.enabled ? 'border-primary' : ''}>
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3 flex-1">
<div className="mt-1">
<CardContent className="pt-4 sm:pt-6">
<div className="flex flex-col sm:flex-row items-start justify-between gap-3 sm:gap-0">
<div className="flex items-start gap-2 sm:gap-3 flex-1 min-w-0 w-full">
<div className="mt-1 shrink-0">
{iconMap[category.id] || <BookOpen className="w-4 h-4" />}
</div>
<div className="flex-1">
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-2 mb-2">
<Label className="font-semibold">{category.name}</Label>
<Label className="font-semibold truncate">{category.name}</Label>
<Switch
checked={category.enabled}
onCheckedChange={async () => {
@@ -834,6 +852,7 @@ export default function SettingsPage() {
variant="ghost"
size="sm"
onClick={() => setEditingCategory(category)}
className="shrink-0"
>
<Edit className="w-4 h-4" />
</Button>
@@ -855,7 +874,7 @@ export default function SettingsPage() {
{/* Edit Category Dialog */}
<Dialog open={!!editingCategory} onOpenChange={(open) => !open && setEditingCategory(null)}>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-[95vw] sm:max-w-2xl w-full">
<DialogHeader>
<DialogTitle>Editar Categoria: {editingCategory?.name}</DialogTitle>
<DialogDescription>
@@ -1077,18 +1096,19 @@ export default function SettingsPage() {
{/* Clear History Dialog */}
<Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
<DialogContent>
<DialogContent className="max-w-[95vw] sm:max-w-md">
<DialogHeader>
<DialogTitle>Confirmar Exclusão</DialogTitle>
<DialogDescription>
Esta ação não pode ser desfeita. Todos os chats serão permanentemente removidos.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
onClick={() => setShowClearDialog(false)}
disabled={isClearing}
className="w-full sm:w-auto"
>
Cancelar
</Button>
@@ -1096,6 +1116,7 @@ export default function SettingsPage() {
variant="destructive"
onClick={handleClearHistory}
disabled={isClearing}
className="w-full sm:w-auto"
>
{isClearing ? (
<>
+70 -35
View File
@@ -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<Task[]>([]);
const [loading, setLoading] = useState(true);
const [isDialogOpen, setIsDialogOpen] = useState(false);
@@ -100,36 +102,25 @@ export default function TasksPage() {
};
return (
<div className="container mx-auto p-6 max-w-6xl">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold">Tarefas Agendadas</h1>
<p className="text-muted-foreground mt-1">
Gerencie tarefas que executam automaticamente em segundo plano
</p>
<div className="container mx-auto p-4 sm:p-6 max-w-6xl w-full overflow-x-hidden">
<div className="mb-4 sm:mb-6">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl font-bold truncate">Tarefas Agendadas</h1>
<p className="text-muted-foreground mt-1 text-sm sm:text-base">
Gerencie tarefas que executam automaticamente em segundo plano
</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => router.push('/chat')}
className="rounded-lg shrink-0"
title="Voltar para o chat"
>
<X className="w-5 h-5" />
</Button>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
Nova Task
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Criar Nova Task</DialogTitle>
<DialogDescription>
Configure uma tarefa para executar automaticamente
</DialogDescription>
</DialogHeader>
<TaskFormDialog
onSuccess={() => {
setIsDialogOpen(false);
loadTasks();
}}
/>
</DialogContent>
</Dialog>
</div>
{loading ? (
@@ -139,16 +130,59 @@ export default function TasksPage() {
) : tasks.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground mb-4">
<p className="text-muted-foreground mb-6">
Nenhuma task agendada ainda.
</p>
<Button onClick={() => setIsDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Criar Primeira Task
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size="lg" className="w-full max-w-md">
<Plus className="w-5 h-5 mr-2" />
Criar Primeira Task
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Criar Nova Task</DialogTitle>
<DialogDescription>
Configure uma tarefa para executar automaticamente
</DialogDescription>
</DialogHeader>
<TaskFormDialog
onSuccess={() => {
setIsDialogOpen(false);
loadTasks();
}}
/>
</DialogContent>
</Dialog>
</CardContent>
</Card>
) : (
<>
<div className="mb-4">
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size="lg" className="w-full">
<Plus className="w-5 h-5 mr-2" />
Nova Task
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Criar Nova Task</DialogTitle>
<DialogDescription>
Configure uma tarefa para executar automaticamente
</DialogDescription>
</DialogHeader>
<TaskFormDialog
onSuccess={() => {
setIsDialogOpen(false);
loadTasks();
}}
/>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{tasks.map(task => (
<Card key={task.id}>
@@ -218,6 +252,7 @@ export default function TasksPage() {
</Card>
))}
</div>
</>
)}
</div>
);
+27 -10
View File
@@ -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({
<DropdownMenuContent align="end" sideOffset={12} className="w-56 rounded-xl">
<DropdownMenuLabel>Fontes de conhecimento</DropdownMenuLabel>
<DropdownMenuSeparator />
{categories.map((cat) => (
<DropdownMenuCheckboxItem
key={cat.id}
checked={cat.enabled}
onCheckedChange={(checked) => onToggleCategory(cat.id, !!checked)}
>
{cat.name}
</DropdownMenuCheckboxItem>
))}
{categories.map((cat) => {
// Mapeamento de ícones por categoria
const getCategoryIcon = (categoryId: string) => {
const iconMap: Record<string, React.ReactNode> = {
'academic': <GraduationCap className="w-4 h-4 text-muted-foreground shrink-0" />,
'news': <Newspaper className="w-4 h-4 text-muted-foreground shrink-0" />,
'tech': <Terminal className="w-4 h-4 text-muted-foreground shrink-0" />,
'finance': <TrendingUp className="w-4 h-4 text-muted-foreground shrink-0" />,
};
return iconMap[categoryId] || null;
};
return (
<DropdownMenuCheckboxItem
key={cat.id}
checked={cat.enabled}
onCheckedChange={(checked) => onToggleCategory(cat.id, !!checked)}
className="pl-8"
>
<div className="flex items-center gap-2.5">
{getCategoryIcon(cat.id)}
<span>{cat.name}</span>
</div>
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
)}
+12 -14
View File
@@ -102,9 +102,9 @@ export function SidebarList({
return (
<TooltipProvider delayDuration={300}>
<div className="flex flex-col h-full border-r border-sidebar-border bg-sidebar min-w-[240px] max-w-[360px] w-full">
<div className="flex flex-col h-full border-r border-sidebar-border bg-sidebar w-full overflow-x-hidden">
{/* Header com busca e botão Nova Conversa */}
<div className="p-3 sm:p-4 border-b border-sidebar-border flex-shrink-0 space-y-2">
<div className="p-3 border-b border-sidebar-border flex-shrink-0 space-y-2.5">
{/* Campo de busca */}
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
@@ -132,7 +132,7 @@ export function SidebarList({
{/* Botão Nova Conversa */}
<Button
onClick={onNewChat}
className="w-full justify-start gap-2 text-sm font-medium h-9 sm:h-10"
className="w-full justify-start gap-2 text-sm font-medium h-9"
variant="outline"
>
<Plus className="w-4 h-4 shrink-0" />
@@ -142,7 +142,7 @@ export function SidebarList({
{/* Lista de chats com scroll customizado */}
<div className="flex-1 overflow-y-auto min-h-0 sidebar-chat-list">
<div className="p-3 sm:p-4 space-y-2 sm:space-y-2.5">
<div className="p-2 space-y-1.5">
{sessions.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
<MessageSquare className="w-10 h-10 text-muted-foreground/40 mb-3" />
@@ -166,7 +166,7 @@ export function SidebarList({
<TooltipTrigger asChild>
<div
className={cn(
"group relative bg-card border rounded-lg cursor-pointer transition-all duration-200",
"sidebar-chat-item group relative bg-card border rounded-lg cursor-pointer transition-all duration-200",
"hover:shadow-md hover:border-primary/20 hover:-translate-y-0.5",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected && "border-primary/40 shadow-sm bg-accent/50",
@@ -183,21 +183,19 @@ export function SidebarList({
}}
>
{/* Conteúdo do card */}
<div className="p-3 sm:p-3.5 flex items-start gap-3 overflow-hidden">
<div className="sidebar-chat-item-content p-2.5">
{/* Ícone/Emoji */}
<div className="shrink-0 flex items-center justify-center w-8 h-8 sm:w-9 sm:h-9 rounded-md bg-muted/50 text-base sm:text-lg">
<div className="shrink-0 flex items-center justify-center w-8 h-8 rounded-md bg-muted/50 text-base">
{session.emoji || '💬'}
</div>
{/* Texto do card */}
<div className="flex-1 min-w-0 overflow-hidden">
<h3
className="text-sm sm:text-base font-medium text-card-foreground truncate leading-tight mb-1"
>
<div className="sidebar-chat-item-text">
<h3 className="sidebar-chat-item-title text-sm font-medium text-card-foreground leading-tight mb-0.5">
{session.title}
</h3>
<p className="text-xs text-muted-foreground">
<p className="sidebar-chat-item-date text-xs text-muted-foreground">
{formatDate(session.updated_at)}
</p>
</div>
@@ -207,7 +205,7 @@ export function SidebarList({
variant="ghost"
size="icon"
className={cn(
"h-7 w-7 sm:h-8 sm:w-8 shrink-0 transition-all duration-200",
"h-7 w-7 shrink-0 transition-all duration-200",
"opacity-0 group-hover:opacity-100",
"hover:bg-destructive/10 hover:text-destructive",
"active:scale-95"
@@ -219,7 +217,7 @@ export function SidebarList({
}}
aria-label={`Deletar conversa "${session.title}"`}
>
<Trash2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>