// sessions.js - Session list management import { send, on } from './ws.js'; import { showContextMenu } from './ui.js'; import { showShareModal } from './modals.js'; export let sessions = []; export let currentSessionId = null; const sessionListeners = new Set(); export function onSessionChange(fn) { sessionListeners.add(fn); return () => sessionListeners.delete(fn); } function notify(event, data) { sessionListeners.forEach(fn => fn(event, data)); } // ── Server events ───────────────────────────────────────────────────────── on('sessions:list', (msg) => { sessions = msg.sessions || []; renderSessions(); }); on('sessions:created', (msg) => { const existing = sessions.findIndex(s => s.id === msg.session.id); if (existing === -1) sessions.unshift(msg.session); else sessions[existing] = msg.session; renderSessions(); notify('created', msg.session); }); on('sessions:deleted', (msg) => { sessions = sessions.filter(s => s.id !== msg.sessionId); if (currentSessionId === msg.sessionId) { currentSessionId = sessions[0]?.id || null; notify('switched', currentSessionId); } renderSessions(); }); on('sessions:deletedAll', () => { sessions = []; currentSessionId = null; renderSessions(); notify('switched', null); }); on('sessions:renamed', (msg) => { const s = sessions.find(s => s.id === msg.sessionId); if (s) s.name = msg.name; renderSessions(); }); on('sessions:data', (msg) => { const existing = sessions.findIndex(s => s.id === msg.session.id); if (existing >= 0) sessions[existing] = msg.session; notify('data', msg.session); }); on('auth:ok', (msg) => { sessions = msg.sessions || []; renderSessions(); if (sessions.length > 0 && !currentSessionId) { switchSession(sessions[0].id); } }); on('auth:guestOk', (msg) => { sessions = msg.sessions || []; renderSessions(); if (sessions.length === 0) { createNewSession(); } else { switchSession(sessions[0].id); } }); on('chat:done', (msg) => { const s = sessions.find(s => s.id === msg.sessionId); if (s) { s.history = msg.history; if (msg.name) s.name = msg.name; sessions.sort((a, b) => { const aTime = a.history?.at(-1)?.timestamp || a.created; const bTime = b.history?.at(-1)?.timestamp || b.created; return bTime - aTime; }); renderSessions(); } }); on('sessions:imported', (msg) => { sessions.unshift(msg.session); renderSessions(); switchSession(msg.session.id); }); // ── Actions ─────────────────────────────────────────────────────────────── export function createNewSession() { send({ type: 'sessions:create' }); } export function switchSession(id) { currentSessionId = id; renderSessions(); send({ type: 'sessions:get', sessionId: id }); notify('switched', id); } export function deleteSession(id) { send({ type: 'sessions:delete', sessionId: id }); } export function deleteAllSessions() { send({ type: 'sessions:deleteAll' }); } export function renameSession(id, name) { send({ type: 'sessions:rename', sessionId: id, name }); const s = sessions.find(s => s.id === id); if (s) s.name = name; renderSessions(); } export function requestSessions() { send({ type: 'sessions:list' }); } export function getCurrentSession() { return sessions.find(s => s.id === currentSessionId) || null; } // ── Render ──────────────────────────────────────────────────────────────── function renderSessions() { const list = document.getElementById('session-list'); if (!list) return; if (sessions.length === 0) { list.innerHTML = `
No chats yet
`; return; } // Group by date const groups = groupByDate(sessions); let html = ''; for (const [label, group] of groups) { html += `
${escHtml(label)}
`; for (const s of group) { const active = s.id === currentSessionId ? ' active' : ''; html += `
${escHtml(s.name || 'New Chat')}
`; } } list.innerHTML = html; // Session click list.querySelectorAll('.session-item').forEach(el => { el.addEventListener('click', (e) => { if (e.target.closest('.session-menu-btn')) return; switchSession(el.dataset.id); }); }); // Menu button list.querySelectorAll('.session-menu-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); openSessionMenu(e, btn.dataset.id); }); }); // Inline rename on name click (double click) list.querySelectorAll('.session-name').forEach(el => { el.addEventListener('dblclick', (e) => { e.stopPropagation(); startInlineRename(el); }); }); } function startInlineRename(el) { const id = el.dataset.id; const original = el.textContent; el.setAttribute('contenteditable', 'true'); el.focus(); document.execCommand('selectAll', false, null); const finish = () => { el.removeAttribute('contenteditable'); const name = el.textContent.trim(); if (name && name !== original) renameSession(id, name); else el.textContent = original; }; el.addEventListener('blur', finish, { once: true }); el.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); el.blur(); } if (e.key === 'Escape') { el.textContent = original; el.blur(); } }); } function openSessionMenu(e, id) { const session = sessions.find(s => s.id === id); const items = [ { label: 'Share', icon: '🔗', onClick: () => showShareModal(id), }, { label: 'Rename', icon: '✏️', onClick: () => { const nameEl = document.querySelector(`.session-name[data-id="${id}"]`); if (nameEl) startInlineRename(nameEl); }, }, { separator: true }, { label: 'Delete', icon: '🗑️', danger: true, onClick: () => deleteSession(id), }, { label: 'Delete All Chats', icon: '⚠️', danger: true, onClick: () => { if (confirm('Delete all chats? This cannot be undone.')) deleteAllSessions(); }, }, ]; showContextMenu(e.clientX, e.clientY, items); } function groupByDate(sessions) { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const yesterday = today - 86400000; const week = today - 6 * 86400000; const groups = new Map([ ['Today', []], ['Yesterday', []], ['This Week', []], ['Older', []], ]); for (const s of sessions) { const t = s.created || 0; if (t >= today) groups.get('Today').push(s); else if (t >= yesterday) groups.get('Yesterday').push(s); else if (t >= week) groups.get('This Week').push(s); else groups.get('Older').push(s); } return [...groups.entries()].filter(([, g]) => g.length > 0); } function escHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }