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
140 lines
4.1 KiB
JavaScript
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, '&')
|
|
.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;
|
|
}
|
|
}
|