Files
the-matrix/js/bark.js
Perplexity Computer a9da7393c7 feat: Workshop interaction layer — chat input, visitor presence, bark display (#40, #41, #42)
Implements the minimum viable conversation loop for Workshop #222:
visitor arrives → sends message → Timmy barks back.

- js/visitor.js: Visitor presence protocol (#41)
  - visitor_entered on load (with device detection: ipad/desktop/mobile)
  - visitor_left on unload or 30s hidden (iPad tab suspend)
  - visitor_message dispatched from chat input
  - visitor_interaction export for future tap-to-interact (#44)
  - Session duration tracking

- js/bark.js: Bark display system (#42)
  - showBark() renders prominent viewport toasts with typing animation
  - Auto-dismiss after display time + typing duration
  - Queue system (max 3 simultaneous, overflow queued)
  - Demo barks in mock mode (Workshop-themed: 222, sovereignty, chain)
  - Barks also logged permanently in chat panel

- index.html: Chat input bar (#40)
  - Terminal-styled input + send button at viewport bottom
  - Enter to send (desktop), button tap (iPad)
  - Safe-area padding for notched devices
  - Chat panel repositioned above input bar
  - Bark container in upper viewport third

- js/websocket.js: New message handlers
  - 'bark' message → showBark() dispatch
  - 'ambient_state' message → placeholder for #43
  - Demo barks start in mock mode

- js/ui.js: appendChatMessage() accepts optional CSS class
  - Visitor messages styled differently from agent messages

Build: 18 modules, 0 errors
Tested: desktop (1280x800) + mobile (390x844) via Playwright

Closes #40, #41, #42
Ref: rockachopa/Timmy-time-dashboard#222, #243
2026-03-19 01:46:04 +00:00

140 lines
4.1 KiB
JavaScript

/**
* 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 = `<div class="bark-agent">${escapeHtml(agentLabel)}</div><span class="bark-text"></span>`;
$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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// ── 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;
}
}