import { router } from '@inertiajs/react'; import { ChevronDown, Clock3, LogOut, MessageSquare, Trash2, } from 'lucide-react'; import type { CSSProperties, ReactNode } from 'react'; import { useState, useSyncExternalStore } from 'react'; import AppearanceToggle from '@/components/appearance-toggle'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { LoadingIndicator } from '@/components/ui/loading-indicator'; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar, } from '@/components/ui/sidebar'; import { clearAuthSession, getStoredAuthUser } from '@/lib/auth'; import type { ChatSessionResponse } from '@/lib/rag'; import { login, mahasiswa } from '@/routes'; import { show as showMahasiswaSession } from '@/routes/mahasiswa'; const sidebarStyle = { '--sidebar-width': '260px', '--sidebar-width-icon': '64px', } as CSSProperties; export type DeleteSessionResult = { ok: boolean; message?: string; }; export function getStoredStudentName(): string { return getStoredAuthUser()?.name ?? 'Mahasiswa'; } function subscribeToAuthStorage(onStoreChange: () => void): () => void { if (typeof window === 'undefined') { return () => {}; } window.addEventListener('storage', onStoreChange); return () => window.removeEventListener('storage', onStoreChange); } export function useStoredStudentName(): string { return useSyncExternalStore( subscribeToAuthStorage, getStoredStudentName, () => 'Mahasiswa', ); } export function isAbortError(error: unknown): boolean { return error instanceof DOMException && error.name === 'AbortError'; } export function formatSessionTime(value?: string): string { if (!value) { return ''; } const normalized = /[Zz]|[+-]\d{2}:?\d{2}$/.test(value) ? value : value + 'Z'; const date = new Date(normalized); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat('en-US', { day: 'numeric', hour: 'numeric', minute: '2-digit', month: 'short', }).format(date); } function getInitials(name: string): string { return name .split(' ') .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase()) .join(''); } function confirmLogout(): void { clearAuthSession(); router.visit(login()); } function StudentSidebarLogo() { const { state, toggleSidebar } = useSidebar(); const handleLogoClick = (): void => { if (state === 'collapsed') { toggleSidebar(); } }; return ( ); } function StudentSidebar({ activeSessionId, chatSessions, isLoading, onDeleteSession, studentName, }: { activeSessionId?: string; chatSessions: ChatSessionResponse[]; isLoading: boolean; onDeleteSession?: ( sessionId: string, ) => DeleteSessionResult | Promise; studentName: string; }) { const [isHistoryOpen, setIsHistoryOpen] = useState(true); const [deleteDialogMessage, setDeleteDialogMessage] = useState(); const [isDeletingSession, setIsDeletingSession] = useState(false); const [isLogoutDialogOpen, setIsLogoutDialogOpen] = useState(false); const [sessionToDelete, setSessionToDelete] = useState(); const { state, toggleSidebar } = useSidebar(); const isSidebarCollapsed = state === 'collapsed'; function handleDeleteDialogOpenChange(open: boolean): void { if (isDeletingSession) { return; } if (!open) { setSessionToDelete(undefined); setDeleteDialogMessage(undefined); } } async function handleConfirmDeleteSession(): Promise { if (!sessionToDelete || !onDeleteSession) { return; } setIsDeletingSession(true); setDeleteDialogMessage(undefined); try { const result = await onDeleteSession(sessionToDelete.uuid_id); if (result.ok) { setSessionToDelete(undefined); return; } setDeleteDialogMessage(result.message); } finally { setIsDeletingSession(false); } } return ( <> {isSidebarCollapsed ? ( { toggleSidebar(); setIsHistoryOpen(true); }} > Riwayat Chat ) : ( <> setIsHistoryOpen( (currentState) => !currentState, ) } type="button" > Riwayat Chat {isHistoryOpen && ( {isLoading && ( )} {chatSessions.map((session) => { const sessionTime = formatSessionTime( session.last_message_at ?? session.updated_at ?? session.created_at, ); return ( router.visit( showMahasiswaSession( session.uuid_id, ), ) } tooltip={ session.title } type="button" > { session.title } {sessionTime && ( { sessionTime } )} {onDeleteSession && ( { event.stopPropagation(); setSessionToDelete( session, ); setDeleteDialogMessage( undefined, ); }} type="button" > )} ); })} )} > )} {getInitials(studentName) || 'AK'} {studentName} Mahasiswa setIsLogoutDialogOpen(true)} tooltip="Logout" > Logout Hapus Riwayat Chat Sesi chat ini akan dihapus dari riwayat. Hapus sesi{' '} {sessionToDelete?.title ?? 'chat ini'} ? {deleteDialogMessage && ( {deleteDialogMessage} )} handleDeleteDialogOpenChange(false)} type="button" variant="outline" > Batal { void handleConfirmDeleteSession(); }} type="button" > {isDeletingSession ? ( ) : ( 'Hapus' )} Keluar dari akun? Anda perlu login kembali untuk mengakses sesi chat. setIsLogoutDialogOpen(false)} type="button" variant="outline" > Batal Logout > ); } export function StudentShell({ activeSessionId, chatSessions, children, isLoadingSessions, onDeleteSession, studentName, }: { activeSessionId?: string; chatSessions: ChatSessionResponse[]; children: ReactNode; isLoadingSessions: boolean; onDeleteSession?: ( sessionId: string, ) => DeleteSessionResult | Promise; studentName: string; }) { return ( RAG Hub router.visit(mahasiswa())} type="button" > + Sesi Baru {children} ); }
Hapus sesi{' '} {sessionToDelete?.title ?? 'chat ini'} ?
{deleteDialogMessage}