MedRAG / frontend /src /components /layout /Sidebar.jsx
hetsheta's picture
Implement Option C logo cropped with minimal borders
8dd3277
Raw
History Blame Contribute Delete
7.79 kB
import { useState, useRef, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useChatStore, useAuthStore } from '../../store'
import {
Plus, MessageSquare, Trash2, Pencil, Check, X,
Stethoscope, LogOut, Shield, ChevronRight, Search
} from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import toast from 'react-hot-toast'
import clsx from 'clsx'
function ConvItem({ conv, isActive, onSelect, onDelete, onRename }) {
const [editing, setEditing] = useState(false)
const [title, setTitle] = useState(conv.title)
const [showActions, setShowActions] = useState(false)
const inputRef = useRef()
useEffect(() => { if (editing) inputRef.current?.focus() }, [editing])
const saveRename = async () => {
if (title.trim() && title !== conv.title) await onRename(conv.id, title.trim())
setEditing(false)
}
const handleKey = (e) => {
if (e.key === 'Enter') saveRename()
if (e.key === 'Escape') { setTitle(conv.title); setEditing(false) }
}
return (
<div
className={clsx('sidebar-item relative', isActive && 'active')}
onClick={() => !editing && onSelect(conv.id)}
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
>
<MessageSquare size={14} className="shrink-0 opacity-60" />
{editing ? (
<input
ref={inputRef}
value={title}
onChange={e => setTitle(e.target.value)}
onKeyDown={handleKey}
onBlur={saveRename}
onClick={e => e.stopPropagation()}
className="flex-1 bg-surface-4 text-white text-sm px-2 py-0.5 rounded outline-none border border-accent/40 min-w-0"
/>
) : (
<span className="flex-1 truncate text-sm">{conv.title}</span>
)}
<AnimatePresence>
{showActions && !editing && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex items-center gap-0.5 shrink-0"
onClick={e => e.stopPropagation()}
>
<button
onClick={() => setEditing(true)}
className="p-1 hover:text-white text-gray-500 rounded hover:bg-white/10"
>
<Pencil size={12} />
</button>
<button
onClick={() => onDelete(conv.id)}
className="p-1 hover:text-red-400 text-gray-500 rounded hover:bg-white/10"
>
<Trash2 size={12} />
</button>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
export default function Sidebar({ onClose }) {
const { conversations, activeConvId, createConversation, selectConversation, deleteConversation, renameConversation, fetchConversations } = useChatStore()
const { user, logout } = useAuthStore()
const navigate = useNavigate()
const [search, setSearch] = useState('')
useEffect(() => { fetchConversations() }, [])
const handleNew = async () => {
await createConversation()
onClose?.()
}
const handleDelete = async (id) => {
if (!confirm('Delete this conversation?')) return
try { await deleteConversation(id) }
catch { toast.error('Could not delete conversation') }
}
const handleRename = async (id, title) => {
try { await renameConversation(id, title) }
catch { toast.error('Could not rename') }
}
const filtered = conversations.filter(c =>
c.title.toLowerCase().includes(search.toLowerCase())
)
// Group by date
const now = new Date()
const groups = { Today: [], 'Last 7 days': [], Older: [] }
filtered.forEach(c => {
const d = new Date(c.updated_at)
const diffDays = (now - d) / 86400000
if (diffDays < 1) groups['Today'].push(c)
else if (diffDays < 7) groups['Last 7 days'].push(c)
else groups['Older'].push(c)
})
return (
<div className="flex flex-col h-full bg-surface-1 w-64 border-r border-white/5">
{/* Logo */}
<div className="px-4 pt-5 pb-3 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<img src="/logo.png" alt="MedRAG Logo" className="w-7 h-7 rounded-lg object-cover shrink-0" />
<span className="font-bold text-white text-sm tracking-tight">MedRAG</span>
</div>
<button
onClick={onClose}
className="p-1 text-gray-500 hover:text-white rounded hover:bg-white/10 md:hidden"
>
<X size={16} />
</button>
</div>
{/* New Chat button */}
<div className="px-3 mb-3">
<button
onClick={handleNew}
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl bg-accent/10 hover:bg-accent/20 border border-accent/20 text-accent text-sm font-medium transition-all duration-150 group"
>
<Plus size={15} className="group-hover:rotate-90 transition-transform duration-200" />
New Chat
</button>
</div>
{/* Search */}
<div className="px-3 mb-2">
<div className="flex items-center gap-2 bg-surface-3 rounded-lg px-3 py-2 border border-white/5">
<Search size={13} className="text-gray-500 shrink-0" />
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search chats…"
className="bg-transparent text-sm text-white placeholder-gray-600 outline-none flex-1 min-w-0"
/>
</div>
</div>
{/* Conversation list */}
<div className="flex-1 overflow-y-auto px-2 pb-2 space-y-4">
{Object.entries(groups).map(([label, convs]) =>
convs.length > 0 && (
<div key={label}>
<p className="px-2 py-1 text-[10px] font-semibold text-gray-600 uppercase tracking-wider">{label}</p>
{convs.map(c => (
<ConvItem
key={c.id}
conv={c}
isActive={c.id === activeConvId}
onSelect={(id) => {
selectConversation(id)
if (window.innerWidth < 768) {
onClose?.()
}
}}
onDelete={handleDelete}
onRename={handleRename}
/>
))}
</div>
)
)}
{filtered.length === 0 && (
<p className="text-center text-gray-600 text-xs py-8">
{search ? 'No chats found' : 'No conversations yet'}
</p>
)}
</div>
{/* Bottom user area */}
<div className="border-t border-white/5 p-3 space-y-1">
{user?.role === 'admin' && (
<button
onClick={() => navigate('/admin')}
className="sidebar-item w-full text-med-purple"
>
<Shield size={14} className="shrink-0" />
Admin Dashboard
<ChevronRight size={12} className="ml-auto" />
</button>
)}
<div className="sidebar-item">
<div className="w-6 h-6 rounded-full bg-accent/20 flex items-center justify-center text-accent text-xs font-bold shrink-0">
{user?.full_name?.[0]?.toUpperCase() ?? 'U'}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-white font-medium truncate">{user?.full_name}</p>
<p className="text-[10px] text-gray-500 truncate">{user?.email}</p>
</div>
<button
onClick={logout}
className="text-gray-500 hover:text-red-400 p-1 rounded shrink-0"
title="Sign out"
>
<LogOut size={13} />
</button>
</div>
</div>
</div>
)
}