import React, { useState, useMemo, useEffect } from 'react';
import { useChatContext } from '../state/ChatProvider';
import { groupSessionsByDate, formatTimeAgo } from '../utils/format';
import type { SessionMeta } from '../state/types';
const DATE_ORDER = ['Today', 'Yesterday', 'This Week', 'Older'];
interface SessionListProps {
variant?: 'overlay' | 'sidebar';
}
export function SessionList({ variant = 'overlay' }: SessionListProps) {
const { state, dispatch, openSessionInTab, renameSession, deleteSession, deleteSessions } = useChatContext();
const [search, setSearch] = useState('');
const [menu, setMenu] = useState<{ session: SessionMeta; x: number; y: number } | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [selectMode, setSelectMode] = useState(false);
const isOverlay = variant === 'overlay';
const filteredSessions = useMemo(() => {
if (!search.trim()) return state.sessions;
const q = search.toLowerCase();
return state.sessions.filter(
(s) =>
(s.name ?? '').toLowerCase().includes(q) ||
(s.title ?? '').toLowerCase().includes(q),
);
}, [state.sessions, search]);
const groups = useMemo(() => groupSessionsByDate(filteredSessions), [filteredSessions]);
useEffect(() => {
setSelectedIds(new Set());
setSelectMode(false);
}, [state.sessions]);
function handleSelect(session: SessionMeta) {
if (selectMode) {
toggleSelect(session);
return;
}
setMenu(null);
openSessionInTab(session.id, session.project_hash);
if (isOverlay) {
dispatch({ type: 'TOGGLE_HISTORY' });
}
}
function toggleSelect(session: SessionMeta) {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(session.id)) {
next.delete(session.id);
} else {
next.add(session.id);
}
return next;
});
}
function handleNewSession() {
setMenu(null);
openSessionInTab();
if (isOverlay) {
dispatch({ type: 'TOGGLE_HISTORY' });
}
}
function handleContextMenu(e: React.MouseEvent, session: SessionMeta) {
e.preventDefault();
e.stopPropagation();
const menuWidth = 132;
const menuHeight = 84;
setMenu({
session,
x: Math.min(e.clientX, window.innerWidth - menuWidth - 8),
y: Math.min(e.clientY, window.innerHeight - menuHeight - 8),
});
}
function enterSelectMode() {
setSelectMode(true);
}
function exitSelectMode() {
setSelectMode(false);
setSelectedIds(new Set());
}
function handleDeleteSelected() {
const toDelete = state.sessions.filter((s) => selectedIds.has(s.id));
if (toDelete.length === 0) return;
deleteSessions(
toDelete.map((s) => ({
id: s.id,
project_hash: s.project_hash,
name: s.name || s.title || '',
})),
);
exitSelectMode();
}
useEffect(() => {
if (!menu && !selectMode) return undefined;
function close() {
setMenu(null);
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (selectMode) {
exitSelectMode();
} else {
close();
}
}
}
function handleClickOutside(e: MouseEvent) {
if (selectMode) {
const target = e.target as HTMLElement;
if (!target.closest('.session-list')) {
exitSelectMode();
}
} else {
close();
}
}
window.addEventListener('click', handleClickOutside);
window.addEventListener('scroll', close, true);
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('click', handleClickOutside);
window.removeEventListener('scroll', close, true);
window.removeEventListener('keydown', handleKeyDown);
};
}, [menu, selectMode]);
const hasSelection = selectMode && selectedIds.size > 0;
const selectedCount = selectedIds.size;
const content = (
<div
className={`session-list session-list-${variant}${selectMode ? ' has-selection' : ''}`}
onClick={(e) => e.stopPropagation()}
>
<div className="session-list-header">
<div className="session-title-row">
<h3>ATOMCODE</h3>
<div className="session-title-actions">
<button
className="select-mode-toggle"
onClick={selectMode ? exitSelectMode : enterSelectMode}
>
{selectMode ? '完成' : '选择'}
</button>
{isOverlay && (
<button className="ghost-btn" onClick={() => dispatch({ type: 'TOGGLE_HISTORY' })} title="Close">
×
</button>
)}
</div>
</div>
<button className="session-new-btn" onClick={handleNewSession}>
<span className="session-new-icon">+</span>
<span>New session</span>
</button>
<input
className="session-search"
type="text"
placeholder="Search sessions..."
value={search}
onChange={(e) => setSearch(e.target.value)}
autoFocus={isOverlay}
/>
</div>
{hasSelection && (
<div className="session-list-selection-bar">
<span className="selection-count">已选 {selectedCount} 项</span>
<button className="selection-bar-btn danger" onClick={handleDeleteSelected}>
删除所选
</button>
<button className="selection-bar-btn" onClick={exitSelectMode}>
取消选择
</button>
</div>
)}
<div className="session-list-body">
{filteredSessions.length === 0 ? (
<div className="session-empty">No sessions yet</div>
) : (
DATE_ORDER.map((label) => {
const items = groups[label];
if (!items || items.length === 0) return null;
return (
<div key={label}>
<div className="session-group-label">{label}</div>
{items.map((s) => {
const isActive = s.id === state.activeSessionId;
const isChecked = selectedIds.has(s.id);
let dotClass = '';
if (!isActive) {
if (s.isGenerating) {
dotClass = 'session-item-dot breathing';
} else if (s.hasUnread) {
dotClass = 'session-item-dot';
}
}
return (
<button
key={`${s.project_hash ?? 'current'}:${s.id}`}
className={`session-item${isActive ? ' active' : ''}${selectMode && isChecked ? ' selected' : ''}`}
onClick={() => handleSelect(s)}
onContextMenu={(e) => handleContextMenu(e, s)}
title={selectMode ? undefined : (s.name || s.title || 'Untitled')}
>
<span
className={`session-item-checkbox${isChecked ? ' checked' : ''}`}
onClick={selectMode ? (e) => {
e.stopPropagation();
e.preventDefault();
toggleSelect(s);
} : undefined}
/>
{dotClass && <span className={dotClass} />}
<span className="session-item-name">
{s.name || s.title || 'Untitled'}
</span>
<span className="session-item-time">
{formatTimeAgo(s.updated_at ?? s.created_at)}
</span>
</button>
);
})}
</div>
);
})
)}
</div>
{menu && (
<div
className="session-context-menu"
style={{ left: menu.x, top: menu.y }}
onClick={(e) => e.stopPropagation()}
onContextMenu={(e) => e.preventDefault()}
>
<button
type="button"
className="session-context-item"
onClick={() => {
renameSession(menu.session);
setMenu(null);
}}
>
修改名称
</button>
<button
type="button"
className="session-context-item danger"
onClick={() => {
deleteSession(menu.session);
setMenu(null);
}}
>
删除会话
</button>
</div>
)}
</div>
);
return (
isOverlay ? (
<div className="session-overlay" onClick={() => dispatch({ type: 'TOGGLE_HISTORY' })}>
{content}
</div>
) : content
);
}