import React, { useState, useEffect } from 'react'; import { useSessions, useCreateSession, useDeleteSession, useRenameSession, type Session, } from './hooks/useSessions'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from './ui/dialog'; import { ScrollArea } from './ui/scroll-area'; import { Trash2, AlertTriangle, RefreshCw, History, Search, Plus, MoreHorizontal, Pencil, Copy, Check, ChevronLeft, Settings, FlaskConical, Moon, Sun, } from 'lucide-react'; import { Alert, AlertDescription } from './ui/alert'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from './ui/dropdown-menu'; import { cn } from '@/lib/utils'; interface SessionPanelProps { isOpen: boolean; onClose: () => void; onExpand?: () => void; currentSessionId?: string | null; onSessionChange: (sessionId: string) => void; returnToWelcome: () => void; variant?: 'inline' | 'overlay'; onSearchOpen?: () => void; onNewChat?: () => void; // App-level actions onSettingsOpen?: () => void; onPlaygroundOpen?: () => void; onThemeToggle?: () => void; theme?: 'light' | 'dark'; } function sortSessions(sessions: Session[]): Session[] { return sessions.sort((a, b) => { const timeA = a.lastActivity ? new Date(a.lastActivity).getTime() : 0; const timeB = b.lastActivity ? new Date(b.lastActivity).getTime() : 0; return timeB - timeA; }); } export default function SessionPanel({ isOpen, onClose, onExpand, currentSessionId, onSessionChange, returnToWelcome, variant = 'overlay', onSearchOpen, onNewChat, onSettingsOpen, onPlaygroundOpen, onThemeToggle, theme, }: SessionPanelProps) { const [isNewSessionOpen, setNewSessionOpen] = useState(false); const [newSessionId, setNewSessionId] = useState(''); const [isDeleteConversationDialogOpen, setDeleteConversationDialogOpen] = useState(false); const [selectedSessionForAction, setSelectedSessionForAction] = useState(null); const [isRenameDialogOpen, setRenameDialogOpen] = useState(false); const [renameValue, setRenameValue] = useState(''); const [copiedSessionId, setCopiedSessionId] = useState(null); const { data: sessionsData = [], isLoading: loading, error } = useSessions(isOpen); // Sort sessions by last activity for display const sessions = sortSessions([...sessionsData]); const createSessionMutation = useCreateSession(); const deleteSessionMutation = useDeleteSession(); const renameSessionMutation = useRenameSession(); // Note: Agent switch invalidation is now handled centrally in AgentSelector // Message/response/title events are handled in useChat via direct cache updates const handleCreateSession = async () => { const newSession = await createSessionMutation.mutateAsync({ sessionId: newSessionId.trim() || undefined, }); setNewSessionId(''); setNewSessionOpen(false); onSessionChange(newSession.id); }; const handleDeleteSession = async (sessionId: string) => { await deleteSessionMutation.mutateAsync({ sessionId }); const isDeletingCurrentSession = currentSessionId === sessionId; if (isDeletingCurrentSession) { returnToWelcome(); } }; const handleDeleteConversation = async () => { if (!selectedSessionForAction) return; await deleteSessionMutation.mutateAsync({ sessionId: selectedSessionForAction }); const isDeletingCurrentSession = currentSessionId === selectedSessionForAction; if (isDeletingCurrentSession) { returnToWelcome(); } setDeleteConversationDialogOpen(false); setSelectedSessionForAction(null); }; const handleOpenRenameDialog = (sessionId: string, currentTitle: string | null) => { setSelectedSessionForAction(sessionId); setRenameValue(currentTitle || ''); setRenameDialogOpen(true); }; const handleRenameSession = async () => { if (!selectedSessionForAction || !renameValue.trim()) return; try { await renameSessionMutation.mutateAsync({ sessionId: selectedSessionForAction, title: renameValue.trim(), }); setRenameDialogOpen(false); setSelectedSessionForAction(null); setRenameValue(''); } catch (error) { // Error is already logged by React Query, keep dialog open for retry console.error(`Failed to rename session: ${error}`); } }; const handleCopySessionId = async (sessionId: string) => { try { await navigator.clipboard.writeText(sessionId); setCopiedSessionId(sessionId); } catch (error) { console.error(`Failed to copy session ID: ${error}`); } }; // Clean up copy feedback timeout useEffect(() => { if (copiedSessionId) { const timeoutId = setTimeout(() => setCopiedSessionId(null), 2000); return () => clearTimeout(timeoutId); } }, [copiedSessionId]); const formatRelativeTime = (timestamp: number | null) => { if (!timestamp) return 'Unknown'; const now = Date.now(); const diff = now - timestamp; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; return `${days}d ago`; }; const content = (
{/* Header with Dexto branding */}
{/* Dexto logo */}
{/* Light mode logo */} Dexto {/* Dark mode logo */} Dexto
{/* Collapse button */}
{/* Error Display */} {error && (
{error.message}
)} {/* Sessions List */} {/* Action items at top of list */}
{onNewChat && ( )} {onSearchOpen && ( )}
{/* Spacer */} {(onNewChat || onSearchOpen) &&
} {/* History Header */} {!loading && sessions.length > 0 && (

History

)} {loading ? (
) : sessions.length === 0 ? (

No conversations yet

Start chatting to see your history

) : (
{sessions.map((session) => { const title = session.title && session.title.trim().length > 0 ? session.title : session.id; const isActive = currentSessionId === session.id; return (
onSessionChange(session.id)} onKeyDown={(e) => { if (e.target !== e.currentTarget) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSessionChange(session.id); } }} >

{title}

{/* Timestamp - hidden on hover */} {formatRelativeTime(session.lastActivity)} {/* Dropdown - shown on hover, positioned to overlap timestamp */} e.stopPropagation()} > handleOpenRenameDialog( session.id, session.title ?? null ) } > Rename handleCopySessionId(session.id)} > {copiedSessionId === session.id ? ( ) : ( )} {copiedSessionId === session.id ? 'Copied!' : 'Copy Session ID'} { if (session.messageCount > 0) { setSelectedSessionForAction(session.id); setDeleteConversationDialogOpen(true); } else { handleDeleteSession(session.id); } }} className="text-destructive focus:text-destructive" > Delete
); })}
)} {/* Footer with app-level actions */}
{/* Developer Tools */} {onPlaygroundOpen && ( )} {/* Separator */} {onPlaygroundOpen && (onThemeToggle || onSettingsOpen) && (
)} {/* Theme Toggle */} {onThemeToggle && ( )} {/* Settings */} {onSettingsOpen && ( )}
{/* New Chat Dialog */} Start New Chat
setNewSessionId(e.target.value)} placeholder="e.g., user-123, project-alpha" className="font-mono" />

Leave empty to auto-generate a unique ID

{/* Delete Conversation Confirmation Dialog */} Delete Conversation This will permanently delete this conversation and all its messages. This action cannot be undone. {selectedSessionForAction && ( Session:{' '} {selectedSessionForAction} )} {/* Rename Session Dialog */} Rename Chat Enter a new name for this conversation.
setRenameValue(e.target.value)} placeholder="Enter chat name..." onKeyDown={(e) => { if (e.key === 'Enter' && renameValue.trim()) { handleRenameSession(); } }} autoFocus />
); // Collapsed sidebar content - thin bar with icon buttons const collapsedContent = (
{/* Dexto icon - click to expand */} {/* Action items with subtle spacing */}
{/* New Chat */} {onNewChat && ( )} {/* Search */} {onSearchOpen && ( )}
{/* Footer actions - playground, theme, settings */}
{/* Playground */} {onPlaygroundOpen && ( )} {/* Theme Toggle */} {onThemeToggle && ( )} {/* Settings */} {onSettingsOpen && ( )}
); // For inline variant, show collapsed or expanded if (variant === 'inline') { if (!isOpen) { // Collapsed state - thin bar return (
{collapsedContent}
); } // Expanded state - full panel return
{content}
; } // Overlay variant with slide animation return ( <> {/* Backdrop */}
{/* Panel */} ); }