/** * static/js/instructions.js * Renders the pending and consumed instruction panels. * Handles add, edit, delete interactions. */ import { state } from './state.js'; import { api } from './api.js'; import { toast } from './app.js'; // ── SVG icon helpers ────────────────────────────────────────────────────── function iconEdit() { return ``; } function iconDelete() { return ``; } function iconCheck() { return ``; } function iconX() { return ``; } // ── Time formatters ─────────────────────────────────────────────────────── function fmtRelativeTime(isoStr) { if (!isoStr) return '–'; const d = new Date(isoStr); const diff = Math.floor((Date.now() - d.getTime()) / 1000); if (diff < 5) return 'just now'; if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return d.toLocaleDateString(); } function fmtAbsTime(isoStr) { if (!isoStr) return '–'; const d = new Date(isoStr); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } /** Refresh all relative-time spans in the lists (called by app.js on a timer). */ export function refreshTimestamps() { document.querySelectorAll('[data-ts]').forEach(el => { el.textContent = fmtRelativeTime(el.dataset.ts); }); } // ── Card renderer ───────────────────────────────────────────────────────── function renderPendingCard(item, index) { const card = document.createElement('div'); card.className = 'instruction-card'; card.dataset.id = item.id; card.style.animationDelay = `${index * 30}ms`; card.innerHTML = `
#${item.position} ${fmtRelativeTime(item.created_at)}
${escapeHtml(item.content)}
`; const editBtn = card.querySelector('.btn--edit'); const deleteBtn = card.querySelector('.btn--delete'); const cancelBtn = card.querySelector('.btn--cancel'); const saveBtn = card.querySelector('.btn--save'); const editArea = card.querySelector('.instruction-card__edit-area'); const content = card.querySelector('.instruction-card__content'); const actions = card.querySelector('.instruction-card__actions'); const editTA = card.querySelector('.edit-textarea'); function showEdit() { editTA.value = item.content; editArea.style.display = 'flex'; content.style.display = 'none'; actions.style.display = 'none'; editTA.focus(); editTA.setSelectionRange(editTA.value.length, editTA.value.length); } function hideEdit() { editArea.style.display = 'none'; content.style.display = ''; actions.style.display = ''; } editBtn.addEventListener('click', showEdit); cancelBtn.addEventListener('click', hideEdit); editTA.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); hideEdit(); } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveBtn.click(); } // Shift+Enter → default browser behaviour (newline) }); saveBtn.addEventListener('click', async () => { const newContent = editTA.value.trim(); if (!newContent) { toast('Content cannot be empty', 'error'); return; } saveBtn.disabled = true; try { await api.updateInstruction(item.id, newContent); toast('Instruction updated', 'success'); } catch (e) { toast(e.message, 'error'); } finally { saveBtn.disabled = false; } }); deleteBtn.addEventListener('click', async () => { if (!confirm('Delete this instruction?')) return; deleteBtn.disabled = true; try { await api.deleteInstruction(item.id); toast('Instruction deleted', 'info'); } catch (e) { toast(e.message, 'error'); deleteBtn.disabled = false; } }); return card; } function renderConsumedCard(item) { const card = document.createElement('div'); card.className = 'instruction-card instruction-card--consumed'; card.dataset.id = item.id; card.innerHTML = `
#${item.position} ${fmtAbsTime(item.consumed_at)} ${item.consumed_by_agent_id ? `→ ${escapeHtml(item.consumed_by_agent_id)}` : ''}
${escapeHtml(item.content)}
`; return card; } // ── List renderers ──────────────────────────────────────────────────────── export function initInstructions() { const pendingList = document.getElementById('pending-list'); const pendingBadge = document.getElementById('pending-badge'); const consumedList = document.getElementById('consumed-list'); const consumedBadge = document.getElementById('consumed-badge'); const clearHistoryBtn = document.getElementById('clear-history-btn'); function render(instructions) { if (!instructions) return; const pending = instructions.filter(i => i.status === 'pending'); const consumed = instructions.filter(i => i.status === 'consumed').reverse(); // Pending pendingList.innerHTML = ''; if (pending.length === 0) { pendingList.innerHTML = `
Queue is empty – add an instruction above
`; } else { pending.forEach((item, i) => pendingList.appendChild(renderPendingCard(item, i))); } pendingBadge.textContent = pending.length; pendingBadge.className = `badge ${pending.length > 0 ? 'badge--cyan' : 'badge--muted'}`; // Consumed consumedList.innerHTML = ''; if (consumed.length === 0) { consumedList.innerHTML = `
No consumed instructions yet
`; clearHistoryBtn.style.display = 'none'; } else { consumed.forEach(item => consumedList.appendChild(renderConsumedCard(item))); clearHistoryBtn.style.display = ''; } consumedBadge.textContent = consumed.length; consumedBadge.className = `badge ${consumed.length > 0 ? 'badge--amber' : 'badge--muted'}`; } state.subscribe('instructions', render); // Clear history button clearHistoryBtn.addEventListener('click', async () => { if (!confirm('Clear all consumed instruction history? This cannot be undone.')) return; clearHistoryBtn.disabled = true; try { const res = await api.clearConsumed(); toast(`Cleared ${res.cleared} consumed instruction${res.cleared !== 1 ? 's' : ''}`, 'info'); } catch (e) { toast(e.message, 'error'); } finally { clearHistoryBtn.disabled = false; } }); } // ── Composer ────────────────────────────────────────────────────────────── export function initComposer() { const form = document.getElementById('composer-form'); const textarea = document.getElementById('composer-input'); const btn = document.getElementById('composer-submit'); textarea.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { // Plain Enter → submit (like a chat box) e.preventDefault(); form.requestSubmit(); } // Shift+Enter → default browser behaviour (newline) }); form.addEventListener('submit', async (e) => { e.preventDefault(); const content = textarea.value.trim(); if (!content) return; btn.disabled = true; const original = btn.innerHTML; btn.innerHTML = ''; try { await api.createInstruction(content); textarea.value = ''; textarea.style.height = ''; toast('Instruction queued', 'success'); } catch (err) { toast(err.message, 'error'); } finally { btn.disabled = false; btn.innerHTML = original; textarea.focus(); } }); // Auto-resize textarea textarea.addEventListener('input', () => { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; }); } // ── Utility ─────────────────────────────────────────────────────────────── function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }