feat: Implement folder management and sidebar collapse

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 18:52:14 +00:00
parent 13d5056869
commit 88722f4148
3 changed files with 220 additions and 30 deletions
+183 -22
View File
@@ -1,5 +1,5 @@
import { useState } from "react";
import { Search, Folder, FolderPlus, MessageSquare, MoreVertical, Trash2, Edit, FolderInput } from "lucide-react";
import { Search, Folder, FolderPlus, MessageSquare, MoreVertical, Trash2, Edit, FolderInput, ChevronLeft, ChevronRight } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -19,6 +19,16 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface Conversation {
id: string;
@@ -31,18 +41,26 @@ interface Conversation {
interface ChatFolder {
id: string;
name: string;
color: string;
}
export const ChatSidebar = () => {
interface ChatSidebarProps {
isCollapsed: boolean;
onToggleCollapse: () => void;
}
export const ChatSidebar = ({ isCollapsed, onToggleCollapse }: ChatSidebarProps) => {
const [searchQuery, setSearchQuery] = useState("");
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
const [isEditFolderOpen, setIsEditFolderOpen] = useState(false);
const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false);
const [newFolderName, setNewFolderName] = useState("");
const [editingFolder, setEditingFolder] = useState<ChatFolder | null>(null);
const [deletingFolder, setDeletingFolder] = useState<ChatFolder | null>(null);
const [selectedConversation, setSelectedConversation] = useState<string | null>("1");
const [folders, setFolders] = useState<ChatFolder[]>([
{ id: "work", name: "Trabalho", color: "from-blue-500 to-cyan-500" },
{ id: "personal", name: "Pessoal", color: "from-purple-500 to-pink-500" },
{ id: "work", name: "Trabalho" },
{ id: "personal", name: "Pessoal" },
]);
const [conversations, setConversations] = useState<Conversation[]>([
@@ -75,7 +93,6 @@ export const ChatSidebar = () => {
const newFolder: ChatFolder = {
id: Date.now().toString(),
name: newFolderName,
color: "from-primary to-secondary",
};
setFolders([...folders, newFolder]);
setNewFolderName("");
@@ -83,6 +100,44 @@ export const ChatSidebar = () => {
}
};
const handleEditFolder = () => {
if (editingFolder && newFolderName.trim()) {
setFolders(
folders.map((f) =>
f.id === editingFolder.id ? { ...f, name: newFolderName } : f
)
);
setNewFolderName("");
setEditingFolder(null);
setIsEditFolderOpen(false);
}
};
const handleDeleteFolder = () => {
if (deletingFolder) {
// Remove folder and move conversations back to "no folder"
setConversations(
conversations.map((conv) =>
conv.folderId === deletingFolder.id ? { ...conv, folderId: undefined } : conv
)
);
setFolders(folders.filter((f) => f.id !== deletingFolder.id));
setDeletingFolder(null);
setIsDeleteFolderOpen(false);
}
};
const openEditFolder = (folder: ChatFolder) => {
setEditingFolder(folder);
setNewFolderName(folder.name);
setIsEditFolderOpen(true);
};
const openDeleteFolder = (folder: ChatFolder) => {
setDeletingFolder(folder);
setIsDeleteFolderOpen(true);
};
const toggleFolder = (folderId: string) => {
const newExpanded = new Set(expandedFolders);
if (newExpanded.has(folderId)) {
@@ -121,10 +176,38 @@ export const ChatSidebar = () => {
return acc;
}, {} as Record<string, Conversation[]>);
if (isCollapsed) {
return (
<div className="w-12 border-r border-border bg-card/30 backdrop-blur-sm flex flex-col items-center py-4">
<Button
variant="ghost"
size="icon"
onClick={onToggleCollapse}
className="text-muted-foreground hover:text-foreground"
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
);
}
return (
<div className="w-80 border-r border-border bg-card/30 backdrop-blur-sm flex flex-col">
{/* Header with collapse button */}
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">Conversas</h3>
<Button
variant="ghost"
size="icon"
onClick={onToggleCollapse}
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
<ChevronLeft className="w-4 h-4" />
</Button>
</div>
{/* Search */}
<div className="p-4 border-b border-border space-y-3">
<div className="px-4 pb-3 border-b border-border space-y-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
@@ -171,6 +254,56 @@ export const ChatSidebar = () => {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Folder Dialog */}
<Dialog open={isEditFolderOpen} onOpenChange={setIsEditFolderOpen}>
<DialogContent className="glass-effect bg-card border-border z-50">
<DialogHeader>
<DialogTitle>Editar Pasta</DialogTitle>
<DialogDescription>
Renomeie sua pasta de conversas.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="edit-folder-name">Nome da Pasta</Label>
<Input
id="edit-folder-name"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Digite o novo nome..."
onKeyDown={(e) => e.key === "Enter" && handleEditFolder()}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditFolderOpen(false)}>
Cancelar
</Button>
<Button onClick={handleEditFolder} disabled={!newFolderName.trim()}>
Salvar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Folder Alert */}
<AlertDialog open={isDeleteFolderOpen} onOpenChange={setIsDeleteFolderOpen}>
<AlertDialogContent className="glass-effect bg-card border-border z-50">
<AlertDialogHeader>
<AlertDialogTitle>Excluir Pasta?</AlertDialogTitle>
<AlertDialogDescription>
Esta ação não pode ser desfeita. As conversas dentro da pasta serão movidas para "Sem Pasta".
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteFolder} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{/* Conversations List */}
@@ -179,22 +312,50 @@ export const ChatSidebar = () => {
{/* Folders */}
{folders.map((folder) => (
<div key={folder.id} className="space-y-1">
<button
onClick={() => toggleFolder(folder.id)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted/50 transition-colors group"
>
<div
className={`w-8 h-8 rounded-lg bg-gradient-to-br ${folder.color} flex items-center justify-center flex-shrink-0`}
<div className="flex items-center gap-1">
<button
onClick={() => toggleFolder(folder.id)}
className="flex-1 flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted/50 transition-colors group"
>
<Folder className="w-4 h-4 text-white" />
</div>
<span className="font-medium text-sm flex-1 text-left">
{folder.name}
</span>
<span className="text-xs text-muted-foreground">
{conversationsByFolder[folder.id]?.length || 0}
</span>
</button>
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<Folder className="w-4 h-4 text-white" />
</div>
<span className="font-medium text-sm flex-1 text-left">
{folder.name}
</span>
<span className="text-xs text-muted-foreground">
{conversationsByFolder[folder.id]?.length || 0}
</span>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground"
>
<MoreVertical className="w-3 h-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
<DropdownMenuItem
className="gap-2"
onClick={() => openEditFolder(folder)}
>
<Edit className="w-3 h-3" />
Editar Nome
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2 text-destructive"
onClick={() => openDeleteFolder(folder)}
>
<Trash2 className="w-3 h-3" />
Excluir Pasta
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{expandedFolders.has(folder.id) && conversationsByFolder[folder.id]?.length > 0 && (
<div className="ml-6 space-y-1">