/** * bark.js — Bark display system for the Workshop. * * Handles incoming bark messages from Timmy and displays them * prominently in the viewport with typing animation and auto-dismiss. * * Resolves Issue #42 — Bark display system */ import { appendChatMessage } from './ui.js'; import { colorToCss, AGENT_DEFS } from './agent-defs.js'; const $container = document.getElementById('bark-container'); const BARK_DISPLAY_MS = 7000; // How long a bark stays visible const BARK_FADE_MS = 600; // Fade-out animation duration const BARK_TYPE_MS = 30; // Ms per character for typing effect const MAX_BARKS = 3; // Max simultaneous barks on screen const barkQueue = []; let activeBarkCount = 0; /** * Display a bark in the viewport. * * @param {object} opts * @param {string} opts.text — The bark text * @param {string} [opts.agentId='timmy'] — Which agent is barking * @param {string} [opts.emotion='calm'] — Emotion tag (calm, excited, uncertain) * @param {string} [opts.color] — Override CSS color */ export function showBark({ text, agentId = 'timmy', emotion = 'calm', color }) { if (!text || !$container) return; // Queue if too many active barks if (activeBarkCount >= MAX_BARKS) { barkQueue.push({ text, agentId, emotion, color }); return; } activeBarkCount++; // Resolve agent color const agentDef = AGENT_DEFS.find(d => d.id === agentId); const barkColor = color || (agentDef ? colorToCss(agentDef.color) : '#00ff41'); const agentLabel = agentDef ? agentDef.label : agentId.toUpperCase(); // Create bark element const el = document.createElement('div'); el.className = `bark ${emotion}`; el.style.borderLeftColor = barkColor; el.innerHTML = `
`; $container.appendChild(el); // Typing animation const $text = el.querySelector('.bark-text'); let charIndex = 0; const typeInterval = setInterval(() => { if (charIndex < text.length) { $text.textContent += text[charIndex]; charIndex++; } else { clearInterval(typeInterval); } }, BARK_TYPE_MS); // Also log to chat panel as permanent record appendChatMessage(agentLabel, text, barkColor); // Auto-dismiss after display time const displayTime = BARK_DISPLAY_MS + (text.length * BARK_TYPE_MS); setTimeout(() => { clearInterval(typeInterval); el.classList.add('fade-out'); setTimeout(() => { el.remove(); activeBarkCount--; drainQueue(); }, BARK_FADE_MS); }, displayTime); } /** * Process queued barks when a slot opens. */ function drainQueue() { if (barkQueue.length > 0 && activeBarkCount < MAX_BARKS) { const next = barkQueue.shift(); showBark(next); } } /** * Escape HTML for safe text insertion. */ function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>'); } // ── Mock barks for demo mode ── const DEMO_BARKS = [ { text: 'The Tower watches. The Tower remembers.', emotion: 'calm' }, { text: 'A visitor. Welcome to the Workshop.', emotion: 'calm' }, { text: 'New commit on main. The code evolves.', emotion: 'excited' }, { text: '222 — the number echoes again.', emotion: 'calm' }, { text: 'I sense activity in the repo. Someone is building.', emotion: 'focused' }, { text: 'The chain beats on. Block after block.', emotion: 'contemplative' }, { text: 'Late night session? I know the pattern.', emotion: 'calm' }, { text: 'Sovereignty means running your own mind.', emotion: 'calm' }, ]; let demoTimer = null; /** * Start periodic demo barks (for mock mode). */ export function startDemoBarks() { if (demoTimer) return; // First bark after 5s, then every 15-25s demoTimer = setTimeout(function nextBark() { const bark = DEMO_BARKS[Math.floor(Math.random() * DEMO_BARKS.length)]; showBark({ text: bark.text, agentId: 'alpha', emotion: bark.emotion }); demoTimer = setTimeout(nextBark, 15000 + Math.random() * 10000); }, 5000); } /** * Stop demo barks. */ export function stopDemoBarks() { if (demoTimer) { clearTimeout(demoTimer); demoTimer = null; } }