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:
1 parent
6a7eeaf7af
commit
ad2ce1e887
6 files changed
+242
-134
No files matched your search
+25
-36
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user