Newer
Older
cortex-hub / ui / client-app / src / components / SessionSidebar.js
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;