File size: 5,140 Bytes
f56a29b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
import type { ChatSession, SessionStatus } from '@/lib/types/chat';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/hooks/use-i18n';
import { ChevronDown, Circle, CheckCircle, Clock } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { ChatSessionComponent } from './chat-session';
interface SessionListProps {
sessions: ChatSession[];
expandedSessionIds: Set<string>;
isStreaming: boolean;
activeBubbleId?: string | null;
onToggleExpand: (sessionId: string) => void;
onEndSession: (sessionId: string) => Promise<void>;
}
const sessionBadgeStyles = {
qa: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
discussion: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
lecture: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
};
// Labels are provided via i18n in the component
function getStatusIcon(status: SessionStatus) {
switch (status) {
case 'active':
return <Circle className="size-2.5 fill-green-500 text-green-500" />;
case 'interrupted':
return <Clock className="size-2.5 text-yellow-500" />;
case 'completed':
return <CheckCircle className="size-2.5 text-gray-400" />;
case 'idle':
default:
return <Circle className="size-2.5 text-gray-300" />;
}
}
export function SessionList({
sessions,
expandedSessionIds,
isStreaming,
activeBubbleId,
onToggleExpand,
onEndSession,
}: SessionListProps) {
const { t } = useI18n();
return (
<>
{sessions.map((session) => {
const isExpanded = expandedSessionIds.has(session.id);
const isActive = session.status === 'active';
const dotColor =
session.type === 'lecture'
? 'bg-purple-500'
: session.type === 'qa'
? 'bg-blue-500'
: 'bg-amber-500';
return (
<div
key={session.id}
className={cn(
'rounded-xl border transition-all duration-500 overflow-hidden',
isActive
? 'border-purple-200 dark:border-purple-700 bg-purple-50/30 dark:bg-purple-900/20 shadow-sm'
: 'border-gray-100 dark:border-gray-800 bg-white/50 dark:bg-gray-800/50',
)}
>
{/* Session Header */}
<button
onClick={() => onToggleExpand(session.id)}
className="w-full flex items-center gap-1.5 px-3 py-2 text-left hover:bg-gray-50/50 dark:hover:bg-gray-800/50 transition-colors"
>
<span className="relative flex h-2.5 w-2.5 shrink-0">
<span className={cn(dotColor, 'relative inline-flex rounded-full h-2.5 w-2.5')} />
{isActive && (
<span
className={cn(
dotColor,
'animate-ping absolute inline-flex h-full w-full rounded-full opacity-75',
)}
/>
)}
</span>
<span
className={cn(
'text-[8px] font-extrabold uppercase tracking-wider px-1.5 py-px rounded shrink-0',
sessionBadgeStyles[session.type],
)}
>
{t(`chat.badge.${session.type}`)}
</span>
<span className="flex-1 text-[11px] font-semibold text-gray-700 dark:text-gray-300 truncate">
{session.title}
</span>
<div className="flex items-center gap-1 text-[9px] text-gray-400 dark:text-gray-500">
{getStatusIcon(session.status)}
</div>
<span className="text-[9px] text-gray-400 dark:text-gray-500 font-medium tabular-nums shrink-0">
{session.messages.length}
</span>
<ChevronDown
className={cn(
'w-3.5 h-3.5 text-gray-400 dark:text-gray-500 transition-transform duration-200 shrink-0',
!isExpanded && '-rotate-90',
)}
/>
</button>
{/* Messages */}
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
className="overflow-hidden border-t border-gray-100/50 dark:border-gray-700/50"
>
<div className="px-2 pb-2 pt-1">
<ChatSessionComponent
session={session}
isActive={isActive}
isStreaming={isStreaming && isActive}
activeBubbleId={activeBubbleId}
onEndSession={onEndSession}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</>
);
}
|