128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { formatDistanceToNow } from 'date-fns'
|
|
import { fr } from 'date-fns/locale/fr'
|
|
import { enUS } from 'date-fns/locale/en-US'
|
|
import { MessageSquare, Trash2, Plus, X } from 'lucide-react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { cn } from '@/lib/utils'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface ChatSidebarProps {
|
|
conversations: any[]
|
|
currentId?: string | null
|
|
onSelect: (id: string) => void
|
|
onNew: () => void
|
|
onDelete?: (id: string) => void
|
|
}
|
|
|
|
export function ChatSidebar({
|
|
conversations,
|
|
currentId,
|
|
onSelect,
|
|
onNew,
|
|
onDelete,
|
|
}: ChatSidebarProps) {
|
|
const { t, language } = useLanguage()
|
|
const dateLocale = language === 'fr' ? fr : enUS
|
|
const [pendingDelete, setPendingDelete] = useState<string | null>(null)
|
|
|
|
const confirmDelete = (id: string) => {
|
|
setPendingDelete(id)
|
|
}
|
|
|
|
const cancelDelete = (e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
setPendingDelete(null)
|
|
}
|
|
|
|
const executeDelete = async (e: React.MouseEvent, id: string) => {
|
|
e.stopPropagation()
|
|
setPendingDelete(null)
|
|
if (onDelete) {
|
|
await onDelete(id)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="w-64 border-r flex flex-col h-full bg-white dark:bg-[#1e2128]">
|
|
<div className="p-4 border-bottom">
|
|
<Button
|
|
onClick={onNew}
|
|
className="w-full justify-start gap-2 shadow-sm"
|
|
variant="outline"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
{t('chat.newConversation')}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
|
{conversations.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground text-sm">
|
|
{t('chat.noHistory')}
|
|
</div>
|
|
) : (
|
|
conversations.map((chat) => (
|
|
<div
|
|
key={chat.id}
|
|
onClick={() => onSelect(chat.id)}
|
|
className={cn(
|
|
"relative cursor-pointer rounded-lg transition-all group",
|
|
currentId === chat.id
|
|
? "bg-primary/10 text-primary dark:bg-primary/20"
|
|
: "hover:bg-muted/50 text-muted-foreground"
|
|
)}
|
|
>
|
|
<div className="p-3 flex flex-col gap-1">
|
|
<div className="flex items-center gap-2">
|
|
<MessageSquare className="h-4 w-4 shrink-0" />
|
|
<span className="truncate text-sm font-medium pr-6">
|
|
{chat.title || t('chat.untitled')}
|
|
</span>
|
|
</div>
|
|
<span className="text-[10px] opacity-60 ml-6">
|
|
{formatDistanceToNow(new Date(chat.updatedAt), { addSuffix: true, locale: dateLocale })}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Delete button — visible on hover or when confirming */}
|
|
{pendingDelete !== chat.id && (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); confirmDelete(chat.id) }}
|
|
className="absolute top-3 right-2 opacity-0 group-hover:opacity-100 p-1 hover:text-destructive transition-all"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
|
|
{/* Inline confirmation banner */}
|
|
{pendingDelete === chat.id && (
|
|
<div
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-destructive/10 text-destructive text-xs border-t border-destructive/20 rounded-b-lg"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<span className="flex-1 font-medium">{t('chat.deleteConfirm')}</span>
|
|
<button
|
|
onClick={(e) => executeDelete(e, chat.id)}
|
|
className="px-2 py-0.5 bg-destructive text-white rounded text-[10px] font-semibold hover:bg-destructive/90 transition-colors"
|
|
>
|
|
{t('chat.yes')}
|
|
</button>
|
|
<button
|
|
onClick={cancelDelete}
|
|
className="p-0.5 hover:text-foreground transition-colors"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|