BarBar288's picture
Upload 122 files
27127dd verified
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;