/** * static/js/shortcuts.js * Quick-send shortcut chips — click to instantly queue a pre-defined * instruction without typing. * * Shortcuts are stored in localStorage so they survive page refreshes * and can be freely added / removed by the user. */ import { api } from './api.js'; import { toast } from './app.js'; const LS_KEY = 'local-mcp-shortcuts'; const DEFAULTS = [ 'Stop and wait for my next instruction', 'Summarize what you just did', 'Explain the last error', 'Undo the last change', 'Continue where you left off', 'Good job! Keep going', ]; // ── Persistence ─────────────────────────────────────────────────────────── function loadShortcuts() { try { const raw = localStorage.getItem(LS_KEY); if (raw) return JSON.parse(raw); } catch { /* ignore */ } return [...DEFAULTS]; } function saveShortcuts(list) { localStorage.setItem(LS_KEY, JSON.stringify(list)); } // ── State ───────────────────────────────────────────────────────────────── let shortcuts = loadShortcuts(); let container = null; // the .shortcuts-row div let editMode = false; // whether delete handles are visible // ── Helpers ─────────────────────────────────────────────────────────────── function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } // ── Rendering ───────────────────────────────────────────────────────────── function renderChip(text, index) { const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'shortcut-chip'; chip.dataset.index = index; chip.title = text; chip.setAttribute('aria-label', `Quick send: ${text}`); chip.innerHTML = ` ${escapeHtml(text)} `; return chip; } function renderAddChip() { const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'shortcut-chip shortcut-chip--add'; chip.id = 'shortcut-add-btn'; chip.title = 'Add a new shortcut'; chip.setAttribute('aria-label', 'Add shortcut'); chip.innerHTML = ` Add `; return chip; } function renderEditToggle() { const btn = document.createElement('button'); btn.type = 'button'; btn.id = 'shortcut-edit-toggle'; btn.className = 'shortcut-edit-toggle'; btn.title = editMode ? 'Done editing' : 'Edit shortcuts'; btn.setAttribute('aria-label', editMode ? 'Done editing shortcuts' : 'Edit shortcuts'); btn.innerHTML = editMode ? ` Done` : ` Edit`; return btn; } export function renderShortcuts() { if (!container) return; container.innerHTML = ''; const header = document.createElement('div'); header.className = 'shortcuts-header'; const label = document.createElement('span'); label.className = 'shortcuts-label'; label.textContent = 'Quick'; header.appendChild(label); const editToggle = renderEditToggle(); header.appendChild(editToggle); editToggle.addEventListener('click', () => { editMode = !editMode; renderShortcuts(); }); container.appendChild(header); const rail = document.createElement('div'); rail.className = 'shortcuts-rail'; shortcuts.forEach((text, i) => { const chip = renderChip(text, i); rail.appendChild(chip); }); if (editMode) { const addChip = renderAddChip(); rail.appendChild(addChip); addChip.addEventListener('click', () => showAddPrompt()); } container.appendChild(rail); // Attach click handlers to chips rail.addEventListener('click', async (e) => { const chip = e.target.closest('.shortcut-chip'); if (!chip || chip.classList.contains('shortcut-chip--add')) return; const delBtn = e.target.closest('.shortcut-chip__del'); if (delBtn) { // Delete mode const idx = parseInt(chip.dataset.index, 10); shortcuts.splice(idx, 1); saveShortcuts(shortcuts); chip.style.animation = 'chip-vanish 180ms ease forwards'; chip.addEventListener('animationend', renderShortcuts, { once: true }); return; } if (editMode) return; // don't send in edit mode // Send the instruction const text = shortcuts[parseInt(chip.dataset.index, 10)]; chip.classList.add('shortcut-chip--firing'); try { await api.createInstruction(text); chip.classList.remove('shortcut-chip--firing'); chip.classList.add('shortcut-chip--sent'); setTimeout(() => chip.classList.remove('shortcut-chip--sent'), 800); toast(`Queued: "${text.slice(0, 40)}${text.length > 40 ? '…' : ''}"`, 'success'); } catch (err) { chip.classList.remove('shortcut-chip--firing'); toast('Failed to queue instruction', 'error'); } }); // Show/hide delete buttons based on editMode container.querySelectorAll('.shortcut-chip__del').forEach(del => { del.style.display = editMode ? 'flex' : 'none'; }); } // ── Add prompt ──────────────────────────────────────────────────────────── function showAddPrompt() { // Remove any existing inline form const existing = document.getElementById('shortcut-add-form'); if (existing) { existing.remove(); return; } const form = document.createElement('div'); form.id = 'shortcut-add-form'; form.className = 'shortcut-add-form'; form.innerHTML = `