Spaces:
Running
Running
import React, { useState, useEffect } from 'react'; | |
import { PlusCircle, MessageSquare, Trash2, Edit2, Save, X, User, LogOut } from 'lucide-react'; | |
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; | |
import { Button } from '@/components/ui/button'; | |
import { Input } from '@/components/ui/input'; | |
import { apiRequest } from '@/lib/queryClient'; | |
import { Conversation } from '@/lib/types'; | |
import { useAuth } from '@/hooks/use-auth'; | |
import { useLocation } from 'wouter'; | |
interface ConversationSidebarProps { | |
isOpen: boolean; | |
onClose: () => void; | |
selectedConversationId: string; | |
onSelectConversation: (conversationId: string) => void; | |
onNewConversation: () => void; | |
} | |
const ConversationSidebar: React.FC<ConversationSidebarProps> = ({ | |
isOpen, | |
onClose, | |
selectedConversationId, | |
onSelectConversation, | |
onNewConversation | |
}) => { | |
const [conversations, setConversations] = useState<Conversation[]>([]); | |
const [isLoading, setIsLoading] = useState(false); | |
const [editingId, setEditingId] = useState<string | null>(null); | |
const [editingTitle, setEditingTitle] = useState(''); | |
const { user, logoutMutation } = useAuth(); | |
const [, setLocation] = useLocation(); | |
const isSignedIn = !!user; | |
// Fetch conversations | |
useEffect(() => { | |
const fetchConversations = async () => { | |
setIsLoading(true); | |
try { | |
const response = await fetch('/api/conversations'); | |
if (response.ok) { | |
const data = await response.json(); | |
setConversations(data); | |
} else { | |
console.error('Failed to fetch conversations'); | |
} | |
} catch (error) { | |
console.error('Error fetching conversations:', error); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
fetchConversations(); | |
// Set up interval to refresh conversations (every 30 seconds) | |
const interval = setInterval(fetchConversations, 30000); | |
return () => clearInterval(interval); | |
}, [isSignedIn]); | |
// Navigate to auth page | |
const handleSignIn = () => { | |
setLocation('/auth'); | |
}; | |
// Sign out | |
const handleSignOut = () => { | |
setLocation('/logout'); | |
}; | |
// Start editing a conversation title | |
const handleEditStart = (conversation: Conversation) => { | |
setEditingId(conversation.id); | |
setEditingTitle(conversation.title); | |
}; | |
// Cancel editing | |
const handleEditCancel = () => { | |
setEditingId(null); | |
setEditingTitle(''); | |
}; | |
// Save edited title | |
const handleEditSave = async (conversationId: string) => { | |
try { | |
const response = await apiRequest('PATCH', `/api/conversations/${conversationId}/title`, { | |
title: editingTitle | |
}); | |
if (response.ok) { | |
const updatedConversation = await response.json(); | |
setConversations(conversations.map(conv => | |
conv.id === conversationId ? updatedConversation : conv | |
)); | |
setEditingId(null); | |
} else { | |
console.error('Failed to update conversation title'); | |
} | |
} catch (error) { | |
console.error('Error updating conversation title:', error); | |
} | |
}; | |
// Delete a conversation | |
const handleDelete = async (conversationId: string) => { | |
// Confirm delete | |
if (!window.confirm('Are you sure you want to delete this conversation?')) { | |
return; | |
} | |
try { | |
const response = await apiRequest('DELETE', `/api/conversations/${conversationId}`); | |
if (response.ok) { | |
setConversations(conversations.filter(conv => conv.id !== conversationId)); | |
// If we deleted the selected conversation, switch to a new one | |
if (conversationId === selectedConversationId) { | |
const nextConv = conversations.find(conv => conv.id !== conversationId); | |
if (nextConv) { | |
onSelectConversation(nextConv.id); | |
} else { | |
onNewConversation(); | |
} | |
} | |
} else { | |
console.error('Failed to delete conversation'); | |
} | |
} catch (error) { | |
console.error('Error deleting conversation:', error); | |
} | |
}; | |
return ( | |
<aside | |
className={`fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 shadow-md transform ${ | |
isOpen ? 'translate-x-0' : '-translate-x-full' | |
} transition-transform duration-300 ease-in-out z-10 flex flex-col`} | |
> | |
<div className="flex items-center justify-between p-4 border-b border-gray-200"> | |
<h2 className="text-lg font-semibold">Conversations</h2> | |
<button | |
onClick={onClose} | |
className="p-1 rounded-full hover:bg-gray-100" | |
aria-label="Close sidebar" | |
> | |
<X className="h-5 w-5 text-gray-500" /> | |
</button> | |
</div> | |
<div className="p-4 border-b border-gray-200"> | |
<Button | |
onClick={onNewConversation} | |
className="w-full flex items-center justify-center" | |
variant="outline" | |
> | |
<PlusCircle className="mr-2 h-4 w-4" /> | |
New Chat | |
</Button> | |
</div> | |
{/* Sign in/out section */} | |
<div className="p-4 border-b border-gray-200"> | |
{isSignedIn ? ( | |
<Button | |
onClick={handleSignOut} | |
variant="ghost" | |
className="w-full flex items-center justify-center text-red-500 hover:text-red-700 hover:bg-red-50" | |
> | |
<LogOut className="mr-2 h-4 w-4" /> | |
Sign Out | |
</Button> | |
) : ( | |
<Button | |
onClick={handleSignIn} | |
variant="default" | |
className="w-full flex items-center justify-center" | |
> | |
<User className="mr-2 h-4 w-4" /> | |
Sign In to Save Chats | |
</Button> | |
)} | |
{!isSignedIn && ( | |
<p className="text-xs text-gray-500 mt-2 text-center"> | |
Create an account to save your conversations | |
</p> | |
)} | |
</div> | |
{/* Conversations list */} | |
<div className="flex-1 overflow-y-auto p-2"> | |
{isLoading ? ( | |
<div className="flex items-center justify-center h-20"> | |
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary"></div> | |
</div> | |
) : ( | |
conversations.length === 0 ? ( | |
isSignedIn ? ( | |
<div className="text-center text-gray-500 py-8"> | |
<MessageSquare className="mx-auto h-12 w-12 text-gray-300 mb-2" /> | |
<p>No conversations yet</p> | |
<p className="text-sm">Start a new chat to get started</p> | |
</div> | |
) : ( | |
<div className="text-center text-gray-500 py-8"> | |
<p className="text-sm">Sign in to view your saved conversations</p> | |
</div> | |
) | |
) : ( | |
<ul className="space-y-1"> | |
{conversations.map(conversation => ( | |
<li key={conversation.id} className="relative"> | |
{editingId === conversation.id ? ( | |
<div className="flex items-center p-2 rounded-md bg-gray-100"> | |
<Input | |
value={editingTitle} | |
onChange={(e) => setEditingTitle(e.target.value)} | |
className="flex-1 mr-1" | |
autoFocus | |
/> | |
<div className="flex"> | |
<Button | |
onClick={() => handleEditSave(conversation.id)} | |
size="sm" | |
variant="ghost" | |
className="p-1 h-8 w-8" | |
> | |
<Save className="h-4 w-4 text-green-500" /> | |
</Button> | |
<Button | |
onClick={handleEditCancel} | |
size="sm" | |
variant="ghost" | |
className="p-1 h-8 w-8" | |
> | |
<X className="h-4 w-4 text-red-500" /> | |
</Button> | |
</div> | |
</div> | |
) : ( | |
<div | |
className={`flex items-center p-2 rounded-md cursor-pointer group ${ | |
conversation.id === selectedConversationId | |
? 'bg-primary text-white' | |
: 'hover:bg-gray-100' | |
}`} | |
onClick={() => onSelectConversation(conversation.id)} | |
> | |
<MessageSquare className={`h-4 w-4 mr-2 ${ | |
conversation.id === selectedConversationId ? 'text-white' : 'text-gray-500' | |
}`} /> | |
<span className="flex-1 truncate">{conversation.title}</span> | |
<div className={`flex space-x-1 ${ | |
conversation.id === selectedConversationId ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' | |
} transition-opacity`}> | |
<TooltipProvider> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
onClick={(e) => { | |
e.stopPropagation(); | |
handleEditStart(conversation); | |
}} | |
size="sm" | |
variant="ghost" | |
className={`p-1 h-6 w-6 ${ | |
conversation.id === selectedConversationId ? 'text-white hover:bg-primary-dark' : '' | |
}`} | |
> | |
<Edit2 className="h-3 w-3" /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>Edit title</p> | |
</TooltipContent> | |
</Tooltip> | |
</TooltipProvider> | |
<TooltipProvider> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
onClick={(e) => { | |
e.stopPropagation(); | |
handleDelete(conversation.id); | |
}} | |
size="sm" | |
variant="ghost" | |
className={`p-1 h-6 w-6 ${ | |
conversation.id === selectedConversationId ? 'text-white hover:bg-primary-dark' : '' | |
}`} | |
> | |
<Trash2 className="h-3 w-3" /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>Delete conversation</p> | |
</TooltipContent> | |
</Tooltip> | |
</TooltipProvider> | |
</div> | |
</div> | |
)} | |
</li> | |
))} | |
</ul> | |
) | |
)} | |
</div> | |
</aside> | |
); | |
}; | |
export default ConversationSidebar; |