// ── Sidebar & main page controller ─────────────────────────────────────────── import { Chat } from './chat.js'; const $ = (sel) => document.querySelector(sel); // ── State ───────────────────────────────────────────────────────────────────── const state = { sessions: [], activeSessionId: null, docs: [], urls: [], /** Map */ selectedContext: new Map(), }; const chat = new Chat(); // ── Boot ────────────────────────────────────────────────────────────────────── async function init() { await Promise.all([loadDocs(), loadUrls(), loadSessions()]); renderContext(); } // ── Documents ───────────────────────────────────────────────────────────────── async function loadDocs() { const res = await fetch('/api/documents/'); state.docs = await res.json(); renderDocs(); } function renderDocs() { const list = $('#doc-list'); list.innerHTML = ''; if (!state.docs.length) { list.innerHTML = '
  • No documents yet
  • '; return; } state.docs.forEach((doc) => { const key = `doc_${doc.id}`; const checked = state.selectedContext.has(key); const li = document.createElement('li'); li.className = 'flex items-center gap-2 group py-0.5 hover:bg-copilot-panel rounded px-1'; li.innerHTML = ` ${esc(doc.original_name)} ${doc.indexed ? '' : ''} `; list.appendChild(li); }); } async function uploadFiles(files) { const indicator = $('#doc-uploading'); indicator.classList.remove('hidden'); for (const file of files) { const fd = new FormData(); fd.append('file', file); await fetch('/api/documents/upload', { method: 'POST', body: fd }); } await loadDocs(); indicator.classList.add('hidden'); } // ── URLs ────────────────────────────────────────────────────────────────────── async function loadUrls() { const res = await fetch('/api/context/urls'); state.urls = await res.json(); renderUrls(); } function renderUrls() { const list = $('#url-list'); list.innerHTML = ''; if (!state.urls.length) { list.innerHTML = '
  • No URLs yet
  • '; return; } state.urls.forEach((u) => { const key = `url_${u.id}`; const checked = state.selectedContext.has(key); const li = document.createElement('li'); li.className = 'flex items-center gap-2 group py-0.5 hover:bg-copilot-panel rounded px-1'; li.innerHTML = ` ${esc(u.title)} ${u.indexed ? '' : ''} `; list.appendChild(li); }); } // ── Sessions ────────────────────────────────────────────────────────────────── async function loadSessions() { const res = await fetch('/api/chat/sessions'); state.sessions = await res.json(); renderSessions(); } function renderSessions() { const list = $('#session-list'); list.innerHTML = ''; if (!state.sessions.length) { list.innerHTML = '
  • No chats yet
  • '; return; } state.sessions.forEach((s) => { const li = document.createElement('li'); const isActive = s.id === state.activeSessionId; li.className = `flex items-center gap-1 group py-0.5 rounded px-1 cursor-pointer ${isActive ? 'bg-copilot-panel text-copilot-accent' : 'hover:bg-copilot-panel text-copilot-text'}`; li.innerHTML = ` ${esc(s.title)} `; li.addEventListener('click', (e) => { if (e.target.closest('[data-delete-session]')) return; selectSession(s.id, s.title); }); list.appendChild(li); }); } async function selectSession(id, title) { state.activeSessionId = id; $('#chat-title').textContent = title; $('#empty-state').classList.add('hidden'); renderSessions(); const res = await fetch(`/api/chat/sessions/${id}/messages`); const msgs = await res.json(); chat.renderHistory(msgs); } // ── Context chips ───────────────────────────────────────────────────────────── function renderContext() { const chips = $('#context-chips'); chips.innerHTML = ''; state.selectedContext.forEach((item, key) => { const chip = document.createElement('span'); chip.className = 'inline-flex items-center gap-1 bg-copilot-panel border border-copilot-border rounded-full px-2 py-0.5 text-xs text-copilot-accent'; chip.innerHTML = `${esc(item.name)} `; chips.appendChild(chip); }); const badge = $('#context-badge'); badge.textContent = state.selectedContext.size ? `${state.selectedContext.size} context source${state.selectedContext.size > 1 ? 's' : ''} active` : ''; } // ── Event delegation ────────────────────────────────────────────────────────── document.addEventListener('change', (e) => { if (e.target.matches('.ctx-checkbox')) { const { id, type } = e.target.dataset; const key = `${type}_${id}`; if (e.target.checked) { const name = type === 'doc' ? state.docs.find((d) => d.id == id)?.original_name ?? `Doc ${id}` : state.urls.find((u) => u.id == id)?.title ?? `URL ${id}`; state.selectedContext.set(key, { id: parseInt(id), type, name }); } else { state.selectedContext.delete(key); } renderContext(); } }); document.addEventListener('click', async (e) => { const delDoc = e.target.closest('[data-delete-doc]'); if (delDoc) { const id = delDoc.dataset.deleteDoc; await fetch(`/api/documents/${id}`, { method: 'DELETE' }); state.selectedContext.delete(`doc_${id}`); renderContext(); await loadDocs(); return; } const delUrl = e.target.closest('[data-delete-url]'); if (delUrl) { const id = delUrl.dataset.deleteUrl; await fetch(`/api/context/urls/${id}`, { method: 'DELETE' }); state.selectedContext.delete(`url_${id}`); renderContext(); await loadUrls(); return; } const delSession = e.target.closest('[data-delete-session]'); if (delSession) { e.stopPropagation(); const id = delSession.dataset.deleteSession; await fetch(`/api/chat/sessions/${id}`, { method: 'DELETE' }); if (state.activeSessionId == id) { state.activeSessionId = null; chat.clear(); $('#chat-title').textContent = 'Select or start a chat'; $('#empty-state').classList.remove('hidden'); } await loadSessions(); return; } const removeCtx = e.target.closest('[data-remove-ctx]'); if (removeCtx) { const key = removeCtx.dataset.removeCtx; state.selectedContext.delete(key); // Uncheck corresponding checkbox const [type, id] = key.split('_'); const cb = document.querySelector(`.ctx-checkbox[data-id="${id}"][data-type="${type}"]`); if (cb) cb.checked = false; renderContext(); } }); // File upload $('#file-input').addEventListener('change', (e) => { if (e.target.files.length) uploadFiles(Array.from(e.target.files)); e.target.value = ''; }); // URL add $('#url-form').addEventListener('submit', async (e) => { e.preventDefault(); const input = $('#url-input'); const url = input.value.trim(); if (!url) return; input.value = ''; await fetch('/api/context/urls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); await loadUrls(); }); // New chat $('#btn-new-chat').addEventListener('click', async () => { const res = await fetch('/api/chat/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'New Chat' }) }); const session = await res.json(); state.sessions.unshift(session); await loadSessions(); selectSession(session.id, session.title); }); // Send message $('#chat-form').addEventListener('submit', async (e) => { e.preventDefault(); if (!state.activeSessionId) { alert('Please start or select a chat first.'); return; } const input = $('#chat-input'); const msg = input.value.trim(); if (!msg) return; input.value = ''; input.style.height = '42px'; const contextIds = Array.from(state.selectedContext.values()).map((c) => ({ id: c.id, type: c.type, })); await chat.send(state.activeSessionId, msg, contextIds); // Refresh session title await loadSessions(); }); // Auto-grow textarea $('#chat-input').addEventListener('input', function () { this.style.height = '42px'; this.style.height = Math.min(this.scrollHeight, 160) + 'px'; }); // Submit on Enter (Shift+Enter = newline) $('#chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); $('#chat-form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); } }); // ── Helpers ─────────────────────────────────────────────────────────────────── function esc(str = '') { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } init();