import React, { useState, useEffect } from 'react';
import {
getUserSessions,
deleteSession,
deleteAllSessions,
getSessionTokenStatus
} from '../services/apiService';
import './SessionSidebar.css';
const SessionSidebar = ({ featureName, currentSessionId, onSwitchSession, onNewSession, refreshTick }) => {
const [isOpen, setIsOpen] = useState(false);
const [sessions, setSessions] = useState([]);
const [tokenHoverData, setTokenHoverData] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', message: '', onConfirm: null });
useEffect(() => {
if (isOpen) fetchSessions();
}, [isOpen, featureName, currentSessionId, refreshTick]);
const fetchSessions = async () => {
setIsLoading(true);
try {
const data = await getUserSessions(featureName);
setSessions(data || []);
} catch (err) {
console.error('Failed to fetch sessions:', err);
} finally {
setIsLoading(false);
}
};
const handleMouseEnter = async (sessionId) => {
if (tokenHoverData[sessionId]) return;
try {
const data = await getSessionTokenStatus(sessionId);
setTokenHoverData(prev => ({ ...prev, [sessionId]: data }));
} catch (err) { /* silent */ }
};
const handleDelete = (e, sessionId) => {
e.stopPropagation();
setConfirmModal({
isOpen: true,
title: 'Delete Session',
message: 'Are you sure you want to delete this session? This action cannot be undone.',
onConfirm: async () => {
try {
await deleteSession(sessionId);
fetchSessions();
if (Number(currentSessionId) === sessionId) {
localStorage.removeItem(`sessionId_${featureName}`);
if (onNewSession) onNewSession();
}
} catch { alert('Failed to delete session.'); }
}
});
};
const handleDeleteAll = (e) => {
if (e) e.stopPropagation();
setConfirmModal({
isOpen: true,
title: 'Clear All History',
message: 'Are you sure you want to delete ALL history for this feature? This action is permanent.',
onConfirm: async () => {
try {
await deleteAllSessions(featureName);
fetchSessions();
if (onNewSession) onNewSession();
} catch { alert('Failed to delete all sessions.'); }
}
});
};
const formatDate = (iso) => {
const d = new Date(iso);
const now = new Date();
const diffDays = Math.floor((now - d) / 86400000);
if (diffDays === 0) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return d.toLocaleDateString([], { weekday: 'short' });
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
};
const prettyFeatureName = featureName
.split('_')
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ');
return (
<div className={`session-sidebar ${isOpen ? 'open' : ''}`}>
{/* ▶/◀ Tab handle */}
<div className="sidebar-toggle" onClick={() => setIsOpen(!isOpen)}>
<span className="sidebar-toggle-arrow">{isOpen ? '◀' : '▶'}</span>
<span className="sidebar-toggle-label">History</span>
</div>
{isOpen && (
<div className="sidebar-content">
<div className="sidebar-header">
<h3>{prettyFeatureName} History</h3>
<button type="button" className="delete-all" onClick={(e) => handleDeleteAll(e)}>
Clear All
</button>
</div>
<div className="sidebar-list">
{isLoading ? (
<p className="sidebar-loading">Loading sessions…</p>
) : sessions.length === 0 ? (
<p className="sidebar-empty">No past sessions yet.</p>
) : (
sessions.map(s => {
const isActive = Number(currentSessionId) === s.id;
const td = tokenHoverData[s.id];
// Derive a display title: prefer session.title, fall back gracefully
const displayTitle = s.title &&
s.title !== 'New Chat Session'
? s.title
: `Session #${s.id}`;
const llmInfo = s.provider_name ? `LLM: ${s.provider_name}` : 'LLM: Default';
const sttInfo = s.stt_provider_name ? `STT: ${s.stt_provider_name}` : 'STT: Default';
const ttsInfo = s.tts_provider_name ? `TTS: ${s.tts_provider_name}` : 'TTS: Default';
const usageInfo = td
? `Context: ${td.token_count.toLocaleString()} / ${td.token_limit.toLocaleString()} tokens (${td.percentage}%)`
: 'Hover to load token usage stats';
const tooltip = `${displayTitle}\n---\n${llmInfo}\n${sttInfo}\n${ttsInfo}\n---\n${usageInfo}`;
return (
<div
key={s.id}
className={`sidebar-item ${isActive ? 'active' : ''}`}
onClick={() => onSwitchSession(s.id)}
onMouseEnter={() => handleMouseEnter(s.id)}
title={tooltip}
>
<div className="sidebar-item-info">
<span className="sidebar-item-title">{displayTitle}</span>
<div className="sidebar-item-meta">
<span className="sidebar-item-date">{formatDate(s.created_at)}</span>
{s.provider_name && (
<span className="sidebar-item-provider">{s.provider_name}</span>
)}
</div>
</div>
<button
type="button"
className="sidebar-item-delete"
onClick={(e) => handleDelete(e, s.id)}
title="Delete this session"
>
×
</button>
</div>
);
})
)}
</div>
</div>
)}
{/* Custom Confirmation Modal */}
{confirmModal.isOpen && (
<div className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm flex justify-center items-center z-[2000] animate-in fade-in duration-300 px-4">
<div className="bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-2xl max-w-sm w-full text-center border border-red-100 dark:border-red-900/30">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</div>
<h2 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">{confirmModal.title}</h2>
<p className="text-gray-500 dark:text-gray-400 mb-6 text-sm">{confirmModal.message}</p>
<div className="flex gap-3">
<button
onClick={() => setConfirmModal({ ...confirmModal, isOpen: false })}
className="flex-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-bold py-3 rounded-xl transition-all active:scale-95 underline-none"
>
Cancel
</button>
<button
onClick={() => {
confirmModal.onConfirm();
setConfirmModal({ ...confirmModal, isOpen: false });
}}
className="flex-1 bg-red-600 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-red-500/20 active:scale-95 underline-none"
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default SessionSidebar;