Files
the-matrix/js/main.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

76 lines
2.0 KiB
JavaScript

import { initWorld, onWindowResize } from './world.js';
import { initAgents, updateAgents, getAgentCount } from './agents.js';
import { initEffects, updateEffects } from './effects.js';
import { initUI, updateUI } from './ui.js';
import { initInteraction, updateControls } from './interaction.js';
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
import { initVisitor } from './visitor.js';
let frameCount = 0;
let lastFpsTime = performance.now();
let currentFps = 0;
function main() {
const { scene, camera, renderer } = initWorld();
initEffects(scene);
initAgents(scene);
initInteraction(camera, renderer);
initUI();
initWebSocket(scene);
initVisitor();
// Debounce resize to 1 call per frame (avoids dozens of framebuffer re-allocations during drag)
let resizeFrame = null;
window.addEventListener('resize', () => {
if (resizeFrame) cancelAnimationFrame(resizeFrame);
resizeFrame = requestAnimationFrame(() => onWindowResize(camera, renderer));
});
// Dismiss loading screen
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) loadingScreen.classList.add('hidden');
let rafId = null;
function animate() {
rafId = requestAnimationFrame(animate);
const now = performance.now();
frameCount++;
if (now - lastFpsTime >= 1000) {
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
frameCount = 0;
lastFpsTime = now;
}
updateControls();
updateEffects(now);
updateAgents(now);
updateUI({
fps: currentFps,
agentCount: getAgentCount(),
jobCount: getJobCount(),
connectionState: getConnectionState(),
});
renderer.render(scene, camera);
}
// Pause rendering when tab is backgrounded (saves battery on iPad PWA)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
} else {
if (!rafId) animate();
}
});
animate();
}
main();