feat: Add conversation history and search to bots
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChatMessage } from "@/components/chat/ChatMessage";
|
import { ChatMessage } from "@/components/chat/ChatMessage";
|
||||||
import { ChatInput } from "@/components/chat/ChatInput";
|
import { ChatInput } from "@/components/chat/ChatInput";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { BotChatSidebar, BotConversation } from "./BotChatSidebar";
|
||||||
|
|
||||||
interface Bot {
|
interface Bot {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,17 +31,49 @@ interface BotChatProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BotChat = ({ bot, onBack }: BotChatProps) => {
|
export const BotChat = ({ bot, onBack }: BotChatProps) => {
|
||||||
const [messages, setMessages] = useState<Message[]>([
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
{
|
const [conversations, setConversations] = useState<BotConversation[]>([]);
|
||||||
|
const [selectedConversationId, setSelectedConversationId] = useState<string | null>(null);
|
||||||
|
const [conversationMessages, setConversationMessages] = useState<Record<string, Message[]>>({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Initialize first conversation
|
||||||
|
useEffect(() => {
|
||||||
|
if (conversations.length === 0) {
|
||||||
|
createNewConversation();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createNewConversation = () => {
|
||||||
|
const newConversation: BotConversation = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
title: "Nova Conversa",
|
||||||
|
lastMessage: "Inicie uma conversa...",
|
||||||
|
timestamp: "Agora",
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialMessage: Message = {
|
||||||
id: "1",
|
id: "1",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "Envie sua mensagem",
|
content: "Envie sua mensagem",
|
||||||
model: bot.model,
|
model: bot.model,
|
||||||
},
|
};
|
||||||
]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
setConversations(prev => [newConversation, ...prev]);
|
||||||
|
setConversationMessages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[newConversation.id]: [initialMessage],
|
||||||
|
}));
|
||||||
|
setSelectedConversationId(newConversation.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentMessages = selectedConversationId
|
||||||
|
? conversationMessages[selectedConversationId] || []
|
||||||
|
: [];
|
||||||
|
|
||||||
const handleSendMessage = (content: string, files?: File[]) => {
|
const handleSendMessage = (content: string, files?: File[]) => {
|
||||||
|
if (!selectedConversationId) return;
|
||||||
|
|
||||||
const newMessage: Message = {
|
const newMessage: Message = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -52,7 +85,28 @@ export const BotChat = ({ bot, onBack }: BotChatProps) => {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
setMessages((prev) => [...prev, newMessage]);
|
// Update messages
|
||||||
|
setConversationMessages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[selectedConversationId]: [...(prev[selectedConversationId] || []), newMessage],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update conversation title and last message
|
||||||
|
setConversations(prev => prev.map(conv => {
|
||||||
|
if (conv.id === selectedConversationId) {
|
||||||
|
const title = conv.title === "Nova Conversa"
|
||||||
|
? content.slice(0, 30) + (content.length > 30 ? "..." : "")
|
||||||
|
: conv.title;
|
||||||
|
return {
|
||||||
|
...conv,
|
||||||
|
title,
|
||||||
|
lastMessage: content,
|
||||||
|
timestamp: "Agora",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return conv;
|
||||||
|
}));
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Simulate AI response
|
// Simulate AI response
|
||||||
@@ -63,12 +117,59 @@ export const BotChat = ({ bot, onBack }: BotChatProps) => {
|
|||||||
content: `Resposta do ${bot.name} usando ${bot.model}. Esta é uma resposta simulada considerando as instruções: "${bot.prompt}"`,
|
content: `Resposta do ${bot.name} usando ${bot.model}. Esta é uma resposta simulada considerando as instruções: "${bot.prompt}"`,
|
||||||
model: bot.model,
|
model: bot.model,
|
||||||
};
|
};
|
||||||
setMessages((prev) => [...prev, aiResponse]);
|
|
||||||
|
setConversationMessages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[selectedConversationId]: [...(prev[selectedConversationId] || []), aiResponse],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update last message in conversation
|
||||||
|
setConversations(prev => prev.map(conv => {
|
||||||
|
if (conv.id === selectedConversationId) {
|
||||||
|
return {
|
||||||
|
...conv,
|
||||||
|
lastMessage: aiResponse.content.slice(0, 50) + "...",
|
||||||
|
timestamp: "Agora",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return conv;
|
||||||
|
}));
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteConversation = (id: string) => {
|
||||||
|
setConversations(prev => prev.filter(conv => conv.id !== id));
|
||||||
|
setConversationMessages(prev => {
|
||||||
|
const newMessages = { ...prev };
|
||||||
|
delete newMessages[id];
|
||||||
|
return newMessages;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If deleting current conversation, select the first available one
|
||||||
|
if (selectedConversationId === id) {
|
||||||
|
const remainingConversations = conversations.filter(conv => conv.id !== id);
|
||||||
|
if (remainingConversations.length > 0) {
|
||||||
|
setSelectedConversationId(remainingConversations[0].id);
|
||||||
|
} else {
|
||||||
|
createNewConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex-1 flex h-full">
|
||||||
|
<BotChatSidebar
|
||||||
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
onToggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
||||||
|
conversations={conversations}
|
||||||
|
selectedConversationId={selectedConversationId}
|
||||||
|
onSelectConversation={setSelectedConversationId}
|
||||||
|
onNewConversation={createNewConversation}
|
||||||
|
onDeleteConversation={handleDeleteConversation}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col h-full">
|
<div className="flex-1 flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b border-border p-4 flex items-center gap-4">
|
<div className="border-b border-border p-4 flex items-center gap-4">
|
||||||
@@ -88,7 +189,7 @@ export const BotChat = ({ bot, onBack }: BotChatProps) => {
|
|||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
{messages.map((message) => (
|
{currentMessages.map((message) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={message.id}
|
key={message.id}
|
||||||
role={message.role}
|
role={message.role}
|
||||||
@@ -116,5 +217,6 @@ export const BotChat = ({ bot, onBack }: BotChatProps) => {
|
|||||||
{/* Input */}
|
{/* Input */}
|
||||||
<ChatInput onSendMessage={handleSendMessage} />
|
<ChatInput onSendMessage={handleSendMessage} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Search, MessageSquare, MoreVertical, Trash2, ChevronLeft, ChevronRight, Plus } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
|
export interface BotConversation {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
lastMessage: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BotChatSidebarProps {
|
||||||
|
isCollapsed: boolean;
|
||||||
|
onToggleCollapse: () => void;
|
||||||
|
conversations: BotConversation[];
|
||||||
|
selectedConversationId: string | null;
|
||||||
|
onSelectConversation: (id: string) => void;
|
||||||
|
onNewConversation: () => void;
|
||||||
|
onDeleteConversation: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BotChatSidebar = ({
|
||||||
|
isCollapsed,
|
||||||
|
onToggleCollapse,
|
||||||
|
conversations,
|
||||||
|
selectedConversationId,
|
||||||
|
onSelectConversation,
|
||||||
|
onNewConversation,
|
||||||
|
onDeleteConversation,
|
||||||
|
}: BotChatSidebarProps) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [deletingConversation, setDeletingConversation] = useState<BotConversation | null>(null);
|
||||||
|
|
||||||
|
const filteredConversations = conversations.filter(
|
||||||
|
(conv) =>
|
||||||
|
conv.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
conv.lastMessage.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteConversation = () => {
|
||||||
|
if (deletingConversation) {
|
||||||
|
onDeleteConversation(deletingConversation.id);
|
||||||
|
setDeletingConversation(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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="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
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Pesquisar conversas..."
|
||||||
|
className="pl-9 bg-muted/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="w-full gap-2 cyber-glow" variant="default" onClick={onNewConversation}>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Nova Conversa
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conversations List */}
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
{filteredConversations.length === 0 ? (
|
||||||
|
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
||||||
|
{searchQuery ? "Nenhuma conversa encontrada" : "Nenhuma conversa ainda"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredConversations.map((conv) => (
|
||||||
|
<ConversationItem
|
||||||
|
key={conv.id}
|
||||||
|
conversation={conv}
|
||||||
|
isSelected={selectedConversationId === conv.id}
|
||||||
|
onSelect={() => onSelectConversation(conv.id)}
|
||||||
|
onDelete={() => setDeletingConversation(conv)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Delete Conversation Alert */}
|
||||||
|
<AlertDialog open={!!deletingConversation} onOpenChange={(open) => !open && setDeletingConversation(null)}>
|
||||||
|
<AlertDialogContent className="glass-effect bg-card border-border z-50">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Excluir Conversa?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Esta ação não pode ser desfeita. A conversa será permanentemente excluída.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDeleteConversation} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||||
|
Excluir
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ConversationItemProps {
|
||||||
|
conversation: BotConversation;
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConversationItem = ({
|
||||||
|
conversation,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
onDelete,
|
||||||
|
}: ConversationItemProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`group flex items-start gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all ${
|
||||||
|
isSelected
|
||||||
|
? "bg-sidebar-accent cyber-border"
|
||||||
|
: "hover:bg-muted/50"
|
||||||
|
}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{conversation.title}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{conversation.lastMessage}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{conversation.timestamp}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<MoreVertical className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
|
||||||
|
<DropdownMenuItem className="gap-2 text-destructive" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete();
|
||||||
|
}}>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
Excluir
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user