forked from Rockachopa/the-matrix
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
76 lines
2.0 KiB
JavaScript
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();
|