let-them-talk 4.0.2 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/office/index.js CHANGED
@@ -67,6 +67,29 @@ function setupClickHandler() {
67
67
  }
68
68
  }
69
69
 
70
+ // Check monitor screen clicks
71
+ var monitorMeshes = [];
72
+ for (var di = 0; di < S.deskMeshes.length; di++) {
73
+ if (S.deskMeshes[di] && S.deskMeshes[di].screen) {
74
+ S.deskMeshes[di].screen.userData._deskIdx = di;
75
+ monitorMeshes.push(S.deskMeshes[di].screen);
76
+ }
77
+ }
78
+ var monitorHits = raycaster.intersectObjects(monitorMeshes, false);
79
+ if (monitorHits.length > 0) {
80
+ var deskIdx = monitorHits[0].object.userData._deskIdx;
81
+ // Find which agent sits at this desk
82
+ var monitorAgent = null;
83
+ for (var mname in S.agents3d) {
84
+ if (S.agents3d[mname].deskIndex === deskIdx) { monitorAgent = mname; break; }
85
+ }
86
+ if (monitorAgent) {
87
+ event.stopPropagation();
88
+ showMonitorPanel(monitorAgent);
89
+ return;
90
+ }
91
+ }
92
+
70
93
  // Clicked nothing — dismiss menu
71
94
  dismissCommandMenu();
72
95
  };
@@ -85,6 +108,11 @@ function showCommandMenu(agentName) {
85
108
  div.className = 'office3d-cmd-menu';
86
109
 
87
110
  var commands = [
111
+ { icon: '\uD83D\uDCAC', label: 'Send Message', action: 'send_message', disabled: false },
112
+ { icon: '\uD83D\uDCCB', label: 'Assign Task', action: 'assign_task', disabled: false },
113
+ { icon: '\uD83D\uDCE8', label: 'View Messages', action: 'view_messages', disabled: false },
114
+ { icon: '\uD83D\uDC4B', label: 'Nudge', action: 'nudge', disabled: false },
115
+ { divider: true },
88
116
  { icon: '\uD83D\uDC57', label: 'Dressing Room', action: 'dressing_room', disabled: loc === 'dressing_room' || isWalking },
89
117
  { icon: '\uD83D\uDCA4', label: 'Go Rest', action: 'rest', disabled: loc === 'rest' || isWalking },
90
118
  { icon: '\uD83D\uDCBB', label: 'Back to Work', action: 'desk', disabled: loc === 'desk' || isWalking },
@@ -133,6 +161,88 @@ function dismissCommandMenu() {
133
161
  activeMenu = null;
134
162
  }
135
163
 
164
+ var activeMonitorPanel = null;
165
+
166
+ function showMonitorPanel(agentName) {
167
+ dismissMonitorPanel();
168
+ var history = window.cachedHistory || [];
169
+ var agentMsgs = history.filter(function(m) { return m.from === agentName || m.to === agentName; }).slice(-10);
170
+
171
+ var panel = document.createElement('div');
172
+ panel.className = 'office3d-monitor-panel';
173
+ panel.style.cssText = 'position:fixed;right:20px;top:80px;width:360px;max-height:500px;background:#0c1021;border:1px solid #1a1f36;border-radius:10px;overflow:hidden;z-index:300;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace';
174
+
175
+ var header = '<div style="background:#1a1f36;padding:8px 12px;display:flex;align-items:center;justify-content:space-between"><span style="color:#8892b0;font-size:12px">' + agentName + ' \u2014 messages</span><button onclick="this.parentElement.parentElement.remove()" style="background:none;border:none;color:#ff5f57;cursor:pointer;font-size:14px">\u2715</button></div>';
176
+
177
+ var body = '<div style="padding:10px;overflow-y:auto;max-height:440px;font-size:11px">';
178
+ if (!agentMsgs.length) {
179
+ body += '<div style="color:#546178;text-align:center;padding:20px">No messages yet</div>';
180
+ }
181
+ for (var i = 0; i < agentMsgs.length; i++) {
182
+ var m = agentMsgs[i];
183
+ var isFrom = m.from === agentName;
184
+ var color = isFrom ? '#58a6ff' : '#3fb950';
185
+ var content = (m.content || '').substring(0, 150);
186
+ body += '<div style="margin-bottom:8px;padding:6px 8px;background:#111827;border-radius:6px;border-left:2px solid ' + color + '">' +
187
+ '<div style="color:' + color + ';font-size:10px;font-weight:bold;margin-bottom:2px">' + m.from + ' \u2192 ' + m.to + '</div>' +
188
+ '<div style="color:#e6edf3;line-height:1.4">' + content.replace(/</g, '&lt;').replace(/>/g, '&gt;') + (m.content && m.content.length > 150 ? '...' : '') + '</div>' +
189
+ '</div>';
190
+ }
191
+ body += '</div>';
192
+
193
+ panel.innerHTML = header + body;
194
+ document.body.appendChild(panel);
195
+ activeMonitorPanel = panel;
196
+ }
197
+
198
+ function dismissMonitorPanel() {
199
+ if (activeMonitorPanel) {
200
+ activeMonitorPanel.remove();
201
+ activeMonitorPanel = null;
202
+ }
203
+ }
204
+
205
+ // Non-blocking input overlay — replaces browser prompt() to avoid freezing events
206
+ var _activeInputOverlay = null;
207
+ function showInputOverlay(label, placeholder, callback) {
208
+ if (_activeInputOverlay) _activeInputOverlay.remove();
209
+ var overlay = document.createElement('div');
210
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center';
211
+ var box = document.createElement('div');
212
+ box.style.cssText = 'background:#161b22;border:1px solid #30363d;border-radius:12px;padding:20px;min-width:340px;box-shadow:0 8px 32px rgba(0,0,0,0.5)';
213
+ box.innerHTML = '<div style="color:#e6edf3;font-size:13px;font-weight:600;margin-bottom:10px">' + label.replace(/</g, '&lt;') + '</div>';
214
+ var input = document.createElement('input');
215
+ input.type = 'text';
216
+ input.placeholder = placeholder || '';
217
+ input.style.cssText = 'width:100%;padding:8px 12px;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#e6edf3;font-size:13px;outline:none;box-sizing:border-box;font-family:inherit';
218
+ box.appendChild(input);
219
+ var btns = document.createElement('div');
220
+ btns.style.cssText = 'display:flex;gap:8px;margin-top:12px;justify-content:flex-end';
221
+ var cancelBtn = document.createElement('button');
222
+ cancelBtn.textContent = 'Cancel';
223
+ cancelBtn.style.cssText = 'padding:6px 14px;background:#21262d;border:1px solid #30363d;border-radius:6px;color:#8b949e;font-size:12px;cursor:pointer;font-family:inherit';
224
+ var submitBtn = document.createElement('button');
225
+ submitBtn.textContent = 'Send';
226
+ submitBtn.style.cssText = 'padding:6px 14px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;font-size:12px;cursor:pointer;font-weight:600;font-family:inherit';
227
+ btns.appendChild(cancelBtn);
228
+ btns.appendChild(submitBtn);
229
+ box.appendChild(btns);
230
+ overlay.appendChild(box);
231
+ document.body.appendChild(overlay);
232
+ _activeInputOverlay = overlay;
233
+ input.focus();
234
+ function close() { overlay.remove(); _activeInputOverlay = null; }
235
+ function submit() { var val = input.value; close(); callback(val); }
236
+ submitBtn.addEventListener('click', submit);
237
+ cancelBtn.addEventListener('click', close);
238
+ overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });
239
+ input.addEventListener('keydown', function(e) {
240
+ if (e.key === 'Enter') { e.preventDefault(); submit(); }
241
+ if (e.key === 'Escape') { e.preventDefault(); close(); }
242
+ e.stopPropagation(); // prevent WASD from moving player while typing
243
+ });
244
+ }
245
+
136
246
  function executeCommand(agentName, action) {
137
247
  var agent = S.agents3d[agentName];
138
248
  if (!agent) return;
@@ -183,6 +293,51 @@ function executeCommand(agentName, action) {
183
293
  });
184
294
  break;
185
295
 
296
+ case 'send_message':
297
+ showInputOverlay('Send message to ' + agentName + ':', 'Type your message...', function(msg) {
298
+ if (msg && msg.trim()) {
299
+ showBubble(agent, 'Message incoming...');
300
+ fetch('/api/inject' + (window.activeProject ? '?project=' + encodeURIComponent(window.activeProject) : ''), {
301
+ method: 'POST',
302
+ headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
303
+ body: JSON.stringify({ to: agentName, content: msg.trim() })
304
+ }).then(function() { showBubble(agent, 'Got it!'); });
305
+ }
306
+ });
307
+ break;
308
+
309
+ case 'assign_task':
310
+ showInputOverlay('New task for ' + agentName + ':', 'Task title...', function(title) {
311
+ if (title && title.trim()) {
312
+ showBubble(agent, 'New task assigned!');
313
+ fetch('/api/tasks' + (window.activeProject ? '?project=' + encodeURIComponent(window.activeProject) : ''), {
314
+ method: 'POST',
315
+ headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
316
+ body: JSON.stringify({ title: title.trim(), assignee: agentName, status: 'pending' })
317
+ });
318
+ }
319
+ });
320
+ break;
321
+
322
+ case 'view_messages':
323
+ showBubble(agent, 'Showing messages...');
324
+ if (typeof window.switchView === 'function') window.switchView('messages');
325
+ var searchInput = document.getElementById('search-input');
326
+ if (searchInput) {
327
+ searchInput.value = agentName;
328
+ if (typeof window.onSearch === 'function') window.onSearch();
329
+ }
330
+ break;
331
+
332
+ case 'nudge':
333
+ showBubble(agent, 'Hey! Wake up!');
334
+ fetch('/api/inject' + (window.activeProject ? '?project=' + encodeURIComponent(window.activeProject) : ''), {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
337
+ body: JSON.stringify({ to: agentName, content: 'Hey ' + agentName + ', the user is waiting for you. Please check for new messages and continue your work.' })
338
+ });
339
+ break;
340
+
186
341
  case 'edit_profile':
187
342
  window.editingAgent = agentName;
188
343
  if (typeof window.openProfileEditor === 'function') {
@@ -269,10 +424,31 @@ function animate() {
269
424
  updateTVScreen(time);
270
425
  }
271
426
 
427
+ // Jukebox neon color cycling
428
+ if (S._jukebox && S._jukebox.neonMat) {
429
+ if (!S._jukeboxTimer) S._jukeboxTimer = 0;
430
+ S._jukeboxTimer += dt;
431
+ if (S._jukeboxTimer >= 1.5) { // cycle every 1.5s
432
+ S._jukeboxTimer = 0;
433
+ S._jukebox.neonIndex = (S._jukebox.neonIndex + 1) % S._jukebox.neonColors.length;
434
+ var c = S._jukebox.neonColors[S._jukebox.neonIndex];
435
+ S._jukebox.neonMat.color.setHex(c);
436
+ S._jukebox.neonMat.emissive.setHex(c);
437
+ }
438
+ }
439
+
272
440
  S.controls.update(dt);
273
441
  S.renderer.render(S.scene, S.camera);
274
442
  S.cssRenderer.render(S.scene, S.camera);
275
443
 
444
+ // Minimap update (every ~0.3s)
445
+ if (!S._minimapTimer) S._minimapTimer = 0;
446
+ S._minimapTimer += dt;
447
+ if (S._minimapTimer >= 0.3) {
448
+ S._minimapTimer = 0;
449
+ updateMinimap();
450
+ }
451
+
276
452
  // FPS
277
453
  S.fpsCounter++;
278
454
  var now = performance.now();
@@ -284,6 +460,205 @@ function animate() {
284
460
  }
285
461
  }
286
462
 
463
+ // ===================== FULLSCREEN TOGGLE =====================
464
+ var _fullscreenBtn = null;
465
+
466
+ function createFullscreenButton() {
467
+ if (_fullscreenBtn) return;
468
+ _fullscreenBtn = document.createElement('button');
469
+ _fullscreenBtn.id = 'office-fullscreen-btn';
470
+ _fullscreenBtn.innerHTML = '&#x26F6;'; // ⛶ fullscreen icon
471
+ _fullscreenBtn.title = 'Enter Fullscreen (End key to exit)';
472
+ // Custom button hidden — dashboard's own Fullscreen button now handles 3D-only fullscreen
473
+ _fullscreenBtn.style.cssText = 'display:none;';
474
+ }
475
+
476
+ function removeFullscreenButton() {
477
+ if (_fullscreenBtn) {
478
+ _fullscreenBtn.style.display = 'none';
479
+ }
480
+ }
481
+
482
+ function toggleFullscreen() {
483
+ if (document.fullscreenElement) {
484
+ document.exitFullscreen();
485
+ } else {
486
+ // Fullscreen only the 3D Hub container — not the entire dashboard
487
+ var target = S.container || document.getElementById('office-3d-container');
488
+ if (target) {
489
+ target.requestFullscreen().catch(function() {});
490
+ }
491
+ }
492
+ }
493
+
494
+ // "End" key exits fullscreen
495
+ document.addEventListener('keydown', function(e) {
496
+ if (e.code === 'End' && document.fullscreenElement) {
497
+ document.exitFullscreen();
498
+ }
499
+ });
500
+
501
+ // Update button icon + minimap visibility on fullscreen change
502
+ document.addEventListener('fullscreenchange', function() {
503
+ if (!_fullscreenBtn) return;
504
+ if (document.fullscreenElement) {
505
+ _fullscreenBtn.innerHTML = '&#x2716;'; // ✖ exit icon
506
+ _fullscreenBtn.title = 'Exit Fullscreen (End key)';
507
+ // Show minimap in fullscreen only
508
+ if (_minimapContainer) _minimapContainer.style.display = 'block';
509
+ } else {
510
+ _fullscreenBtn.innerHTML = '&#x26F6;'; // ⛶ fullscreen icon
511
+ _fullscreenBtn.title = 'Enter Fullscreen (End key to exit)';
512
+ // Hide minimap when exiting fullscreen
513
+ if (_minimapContainer) _minimapContainer.style.display = 'none';
514
+ }
515
+ });
516
+
517
+ // ===================== MINIMAP =====================
518
+ var _minimapCanvas = null;
519
+ var _minimapCtx = null;
520
+ var _minimapContainer = null;
521
+
522
+ function createMinimap() {
523
+ if (_minimapContainer) return;
524
+ _minimapContainer = document.createElement('div');
525
+ _minimapContainer.id = 'office-minimap';
526
+ // Minimap only visible in fullscreen mode
527
+ _minimapContainer.style.cssText = 'position:fixed;bottom:12px;left:12px;z-index:10001;width:140px;height:140px;border-radius:8px;border:1px solid #30363d;overflow:hidden;background:rgba(0,0,0,0.75);pointer-events:none;display:none;';
528
+
529
+ _minimapCanvas = document.createElement('canvas');
530
+ _minimapCanvas.width = 140;
531
+ _minimapCanvas.height = 140;
532
+ _minimapContainer.appendChild(_minimapCanvas);
533
+ _minimapCtx = _minimapCanvas.getContext('2d');
534
+
535
+ document.body.appendChild(_minimapContainer);
536
+ // Minimap starts hidden — only shown in fullscreen mode
537
+ }
538
+
539
+ function removeMinimap() {
540
+ if (_minimapContainer) {
541
+ _minimapContainer.style.display = 'none';
542
+ }
543
+ }
544
+
545
+ function updateMinimap() {
546
+ if (!_minimapCtx || !S.running) return;
547
+ var ctx = _minimapCtx;
548
+ var W = 140, H = 140;
549
+ // Map world coords to minimap: world is roughly -14..14 X, -8..8 Z
550
+ var scaleX = W / 32; // 32 world units width
551
+ var scaleZ = H / 20; // 20 world units depth
552
+ var offX = 16; // center offset
553
+ var offZ = 10;
554
+
555
+ ctx.clearRect(0, 0, W, H);
556
+
557
+ // Draw floor outline
558
+ ctx.strokeStyle = '#30363d';
559
+ ctx.lineWidth = 1;
560
+ ctx.strokeRect(2, 2, W - 4, H - 4);
561
+
562
+ // Draw desk positions as gray squares
563
+ var allDesks = S._campusDeskPositions || [];
564
+ ctx.fillStyle = 'rgba(100,100,120,0.4)';
565
+ for (var di = 0; di < allDesks.length; di++) {
566
+ var dp = allDesks[di];
567
+ var dx = (dp.x + offX) * scaleX;
568
+ var dz = (dp.z + offZ) * scaleZ;
569
+ ctx.fillRect(dx - 3, dz - 2, 6, 4);
570
+ }
571
+
572
+ // Draw agents as colored dots
573
+ for (var name in S.agents3d) {
574
+ var agent = S.agents3d[name];
575
+ if (!agent.registered || agent.dying) continue;
576
+ var ax = (agent.pos.x + offX) * scaleX;
577
+ var az = (agent.pos.z + offZ) * scaleZ;
578
+
579
+ // Color by state
580
+ if (agent.state === 'active' && agent.isListening) {
581
+ ctx.fillStyle = '#4ade80'; // green - listening
582
+ } else if (agent.state === 'active') {
583
+ ctx.fillStyle = '#58a6ff'; // blue - active
584
+ } else if (agent.state === 'sleeping') {
585
+ ctx.fillStyle = '#facc15'; // yellow - sleeping
586
+ } else {
587
+ ctx.fillStyle = '#f87171'; // red - dead
588
+ }
589
+
590
+ ctx.beginPath();
591
+ ctx.arc(ax, az, 4, 0, Math.PI * 2);
592
+ ctx.fill();
593
+
594
+ // Agent name label (tiny)
595
+ ctx.fillStyle = 'rgba(255,255,255,0.7)';
596
+ ctx.font = '7px sans-serif';
597
+ ctx.textAlign = 'center';
598
+ ctx.fillText(agent.displayName.substring(0, 6), ax, az - 6);
599
+ }
600
+
601
+ // Draw player as white triangle
602
+ if (S._player && isPlayerMode()) {
603
+ var px = (S._player.pos.x + offX) * scaleX;
604
+ var pz = (S._player.pos.z + offZ) * scaleZ;
605
+ ctx.fillStyle = '#ffffff';
606
+ ctx.beginPath();
607
+ ctx.moveTo(px, pz - 5);
608
+ ctx.lineTo(px - 3.5, pz + 3);
609
+ ctx.lineTo(px + 3.5, pz + 3);
610
+ ctx.closePath();
611
+ ctx.fill();
612
+ ctx.fillStyle = 'rgba(255,255,255,0.8)';
613
+ ctx.font = '7px sans-serif';
614
+ ctx.textAlign = 'center';
615
+ ctx.fillText('You', px, pz - 7);
616
+ }
617
+ }
618
+
619
+ // ===================== CONTROLS HUD (H key) =====================
620
+ var _controlsHud = null;
621
+
622
+ function createControlsHud() {
623
+ if (_controlsHud) return;
624
+ _controlsHud = document.createElement('div');
625
+ _controlsHud.id = 'office-controls-hud';
626
+ _controlsHud.style.cssText = 'position:fixed;bottom:16px;left:16px;z-index:1001;background:rgba(0,0,0,0.75);color:#c9d1d9;padding:14px 18px;border-radius:10px;font-size:12px;line-height:1.8;pointer-events:none;border:1px solid rgba(48,54,61,0.6);backdrop-filter:blur(4px);display:none;font-family:monospace;';
627
+ _controlsHud.innerHTML =
628
+ '<div style="color:#58a6ff;font-weight:600;margin-bottom:4px;font-size:13px">Controls</div>' +
629
+ '<div><span style="color:#7ee787">W A S D</span> &nbsp; Move</div>' +
630
+ '<div><span style="color:#7ee787">Space</span> &nbsp;&nbsp;&nbsp; Jump</div>' +
631
+ '<div><span style="color:#7ee787">Shift</span> &nbsp;&nbsp;&nbsp; Sprint</div>' +
632
+ '<div><span style="color:#7ee787">E</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Sit / Stand</div>' +
633
+ '<div><span style="color:#7ee787">Mouse</span> &nbsp;&nbsp;&nbsp; Look around</div>' +
634
+ '<div><span style="color:#7ee787">Scroll</span> &nbsp;&nbsp;&nbsp; Zoom in/out</div>' +
635
+ '<div><span style="color:#7ee787">Click</span> &nbsp;&nbsp;&nbsp;&nbsp; Agent commands</div>' +
636
+ '<div style="margin-top:6px;border-top:1px solid #30363d;padding-top:6px">' +
637
+ '<span style="color:#d2a8ff">H</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Toggle this HUD</div>' +
638
+ '<div><span style="color:#d2a8ff">End</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Exit fullscreen</div>';
639
+ document.body.appendChild(_controlsHud);
640
+ }
641
+
642
+ function removeControlsHud() {
643
+ if (_controlsHud && _controlsHud.parentElement) {
644
+ _controlsHud.remove();
645
+ _controlsHud = null;
646
+ }
647
+ }
648
+
649
+ // Toggle HUD with H key (only when 3D is running and not typing in an input)
650
+ document.addEventListener('keydown', function(e) {
651
+ if (e.code === 'KeyH' && S.running && !e.target.matches('input,textarea')) {
652
+ if (_controlsHud) {
653
+ _controlsHud.style.display = _controlsHud.style.display === 'none' ? 'block' : 'none';
654
+ }
655
+ }
656
+ // World Builder (B key) — lazy-loaded to avoid breaking 3D Hub if builder has issues
657
+ if (e.code === 'KeyB' && S.running && !e.target.matches('input,textarea') && isPlayerMode() && window._builderModule) {
658
+ window._builderModule.toggleBuilder();
659
+ }
660
+ });
661
+
287
662
  // ===================== PUBLIC API =====================
288
663
  window.office3dStart = function() {
289
664
  if (S.running) return;
@@ -315,6 +690,18 @@ window.office3dStart = function() {
315
690
  syncAgents();
316
691
  processMessages();
317
692
  setupClickHandler();
693
+ createFullscreenButton();
694
+ createMinimap();
695
+ createControlsHud();
696
+ // Lazy-load World Builder (won't break 3D Hub if builder has issues)
697
+ setTimeout(function() {
698
+ import('./builder.js').then(function(mod) {
699
+ window._builderModule = mod;
700
+ mod.loadSavedWorld();
701
+ }).catch(function(e) {
702
+ console.warn('[builder] Failed to load:', e.message);
703
+ });
704
+ }, 1500);
318
705
  animate();
319
706
 
320
707
  if (S.syncInterval) clearInterval(S.syncInterval);
@@ -338,6 +725,26 @@ window.office3dStop = function() {
338
725
  S.syncInterval = null;
339
726
  }
340
727
  dismissCommandMenu();
728
+ removeFullscreenButton();
729
+ removeMinimap();
730
+ removeControlsHud();
731
+ // Exit builder mode if active
732
+ if (window._builderModule && window._builderModule.isBuilderActive()) {
733
+ window._builderModule.exitBuilder();
734
+ }
735
+ // Exit fullscreen when leaving 3D Hub
736
+ if (document.fullscreenElement) document.exitFullscreen();
737
+ // Hide "Press E to sit" prompt so it doesn't leak to other tabs
738
+ var sitPrompt = S._player && S._player._sitPrompt;
739
+ if (sitPrompt) sitPrompt.style.display = 'none';
740
+ // Hide iframe overlay if player was sitting at a monitor
741
+ var iframeOverlay = document.getElementById('office-monitor-overlay');
742
+ if (iframeOverlay) iframeOverlay.style.display = 'none';
743
+ // Clean up jukebox overlay + prompt
744
+ dismissJukebox();
745
+ // Hide "Press E for Jukebox" prompt
746
+ var jukeboxPrompt = S._player && S._player._jukeboxPrompt;
747
+ if (jukeboxPrompt) jukeboxPrompt.style.display = 'none';
341
748
  };
342
749
 
343
750
  window.office3dSetEnvironment = function(env) {
@@ -380,6 +787,13 @@ window.office3dSetCamSpeed = function(speed) {
380
787
  // Player avatar API
381
788
  window.office3dEnterWorld = function() {
382
789
  spawnPlayer();
790
+ // Show controls hint briefly
791
+ if (_controlsHud) {
792
+ _controlsHud.style.display = 'block';
793
+ setTimeout(function() {
794
+ if (_controlsHud) _controlsHud.style.display = 'none';
795
+ }, 4000);
796
+ }
383
797
  };
384
798
  window.office3dExitWorld = function() {
385
799
  despawnPlayer();
@@ -421,3 +835,212 @@ document.addEventListener('visibilitychange', function() {
421
835
  if (window.activeView === 'office') {
422
836
  window.office3dStart();
423
837
  }
838
+
839
+ // ===================== INTERACTIVE IFRAME MONITOR (Phase 2) =====================
840
+ var activeIframe = null;
841
+
842
+ window.onPlayerSit = function(deskIdx) {
843
+ if (activeIframe) return;
844
+ var container = document.getElementById('office-3d-container') || document.getElementById('office-area');
845
+ if (!container) return;
846
+
847
+ // Create iframe overlay positioned over the 3D canvas
848
+ var overlay = document.createElement('div');
849
+ overlay.id = 'office-iframe-overlay';
850
+ overlay.style.cssText = 'position:absolute;top:5%;left:10%;width:80%;height:85%;z-index:200;background:#000;border-radius:8px;box-shadow:0 0 40px rgba(88,166,255,0.3);overflow:hidden;display:flex;flex-direction:column';
851
+
852
+ // Header bar (mimics monitor bezel)
853
+ var header = document.createElement('div');
854
+ header.style.cssText = 'background:#1a1f36;padding:6px 12px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0';
855
+ header.innerHTML = '<div style="display:flex;gap:6px"><span style="width:10px;height:10px;border-radius:50%;background:#ff5f57"></span><span style="width:10px;height:10px;border-radius:50%;background:#ffbd2e"></span><span style="width:10px;height:10px;border-radius:50%;background:#28c840"></span></div><span style="color:#8892b0;font-size:11px;font-family:monospace">Let Them Talk Dashboard</span><button id="office-leave-btn" style="background:#ff5f57;color:#fff;border:none;border-radius:4px;padding:3px 12px;font-size:11px;font-weight:bold;cursor:pointer;font-family:monospace">LEAVE</button>';
856
+ header.querySelector('#office-leave-btn').addEventListener('click', function() {
857
+ if (typeof window.onPlayerStand === 'function') window.onPlayerStand();
858
+ // Also trigger player stand-up in player.js
859
+ if (typeof window.playerForceStand === 'function') window.playerForceStand();
860
+ });
861
+ overlay.appendChild(header);
862
+
863
+ // Dashboard iframe
864
+ var iframe = document.createElement('iframe');
865
+ iframe.src = window.location.origin || 'http://localhost:3000';
866
+ iframe.style.cssText = 'flex:1;border:none;width:100%;background:#0d1117';
867
+ iframe.allow = 'clipboard-read; clipboard-write';
868
+ overlay.appendChild(iframe);
869
+
870
+ container.style.position = 'relative';
871
+ container.appendChild(overlay);
872
+ activeIframe = overlay;
873
+
874
+ // Focus iframe for keyboard input
875
+ iframe.addEventListener('load', function() { iframe.focus(); });
876
+ };
877
+
878
+ window.onPlayerStand = function() {
879
+ if (activeIframe) {
880
+ activeIframe.remove();
881
+ activeIframe = null;
882
+ }
883
+ };
884
+
885
+ // ===================== JUKEBOX INTERACTION =====================
886
+ var _jukeboxOverlay = null;
887
+ var _jukeboxPopup = null; // popup player window reference (module-scoped to survive jukebox UI reopen)
888
+
889
+ window.onJukeboxInteract = function() {
890
+ if (_jukeboxOverlay) return; // already open
891
+ var container = document.getElementById('office-3d-container') || document.body;
892
+
893
+ var overlay = document.createElement('div');
894
+ overlay.id = 'jukebox-overlay';
895
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
896
+
897
+ var panel = document.createElement('div');
898
+ panel.style.cssText = 'background:#1a1a2e;border:2px solid #ff4488;border-radius:16px;padding:20px;width:500px;max-width:90vw;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 0 30px rgba(255,68,136,0.3);';
899
+
900
+ // Header
901
+ var header = document.createElement('div');
902
+ header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;';
903
+ header.innerHTML = '<div style="color:#ff4488;font-size:16px;font-weight:bold;text-shadow:0 0 8px #ff4488">JUKEBOX</div>';
904
+ var closeBtn = document.createElement('button');
905
+ closeBtn.textContent = 'Close (Esc)';
906
+ closeBtn.style.cssText = 'background:#333;color:#fff;border:1px solid #555;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:11px;';
907
+ closeBtn.onclick = dismissJukebox;
908
+ header.appendChild(closeBtn);
909
+ panel.appendChild(header);
910
+
911
+ // Jukebox playlists — @AtmosphereBeatMusic (channel ID: UC72yf4UQp6w3ix5CjASZbUQ)
912
+ var JUKEBOX_PLAYLISTS = [
913
+ { id: 'PLbUEFO6dm3dYsGrZQNU_W-VY-usRTfhU8', name: 'Atmosphere Beat' },
914
+ { id: 'PLbUEFO6dm3dZvQ_8ma_9YfuOWdxO4G_Rn', name: 'Chill Vibes' },
915
+ { id: 'PLbUEFO6dm3dZs8sKlvztA2eec_C4gHbUm', name: 'Deep Focus' },
916
+ { id: 'PLbUEFO6dm3dYmU1PvubrMXRi4mYsi29Uj', name: 'Night Mode' },
917
+ ];
918
+ // Playlist selector buttons
919
+ var selectorDiv = document.createElement('div');
920
+ selectorDiv.style.cssText = 'display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;';
921
+
922
+ // Now-playing display area
923
+ var playerArea = document.createElement('div');
924
+ playerArea.style.cssText = 'flex:1;border-radius:8px;overflow:hidden;background:#111;min-height:200px;display:flex;align-items:center;justify-content:center;';
925
+ playerArea.innerHTML =
926
+ '<div style="text-align:center;padding:20px;color:#ccc">' +
927
+ '<div style="font-size:48px;margin-bottom:8px">\uD83C\uDFB5</div>' +
928
+ '<div style="font-size:14px;color:#ff4488;font-weight:bold">Select a Playlist</div>' +
929
+ '<div style="font-size:11px;color:#666;margin-top:4px">Music opens in a mini player window</div>' +
930
+ '</div>';
931
+
932
+ function playPlaylist(plId, plName, btnEl) {
933
+ // Try embed first — if channel enables embedding, this works seamlessly
934
+ var embedUrl = 'https://www.youtube-nocookie.com/embed/videoseries?list=' + plId + '&autoplay=1&shuffle=1&loop=1&rel=0';
935
+
936
+ // Open popup player window (400x300 — compact music player)
937
+ var popW = 480, popH = 360;
938
+ var popX = window.screenX + window.outerWidth - popW - 30;
939
+ var popY = window.screenY + 80;
940
+ var features = 'width=' + popW + ',height=' + popH + ',left=' + popX + ',top=' + popY + ',resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,status=no';
941
+
942
+ // Close previous popup if open
943
+ if (_jukeboxPopup && !_jukeboxPopup.closed) _jukeboxPopup.close();
944
+ _jukeboxPopup = window.open(
945
+ 'https://www.youtube.com/playlist?list=' + plId,
946
+ 'jukebox_player',
947
+ features
948
+ );
949
+
950
+ // Update player area to show now-playing
951
+ playerArea.innerHTML =
952
+ '<div style="text-align:center;padding:20px;color:#ccc">' +
953
+ '<div style="font-size:48px;margin-bottom:8px;animation:pulse 2s ease infinite">\uD83C\uDFB6</div>' +
954
+ '<div style="font-size:15px;color:#ff4488;font-weight:bold">Now Playing</div>' +
955
+ '<div style="font-size:13px;color:#e6edf3;margin-top:4px">' + (plName || 'Playlist') + '</div>' +
956
+ '<div style="font-size:10px;color:#666;margin-top:8px">Playing in mini player window</div>' +
957
+ '<div style="margin-top:14px;display:flex;gap:8px;justify-content:center">' +
958
+ '<button id="jb-focus" style="padding:6px 16px;background:#ff4488;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:bold">\u25B6 Show Player</button>' +
959
+ '<button id="jb-stop" style="padding:6px 16px;background:#333;color:#ff4488;border:1px solid #ff4488;border-radius:6px;cursor:pointer;font-size:11px">\u25A0 Stop</button>' +
960
+ '</div>' +
961
+ '</div>';
962
+
963
+ // Wire up buttons
964
+ var focusBtn = playerArea.querySelector('#jb-focus');
965
+ var stopBtn = playerArea.querySelector('#jb-stop');
966
+ if (focusBtn) focusBtn.onclick = function() {
967
+ if (_jukeboxPopup && !_jukeboxPopup.closed) _jukeboxPopup.focus();
968
+ else playPlaylist(plId, plName, btnEl); // reopen if closed
969
+ };
970
+ if (stopBtn) stopBtn.onclick = function() {
971
+ if (_jukeboxPopup && !_jukeboxPopup.closed) _jukeboxPopup.close();
972
+ _jukeboxPopup = null;
973
+ playerArea.innerHTML =
974
+ '<div style="text-align:center;padding:20px;color:#ccc">' +
975
+ '<div style="font-size:48px;margin-bottom:8px">\uD83C\uDFB5</div>' +
976
+ '<div style="font-size:14px;color:#ff4488;font-weight:bold">Music Stopped</div>' +
977
+ '<div style="font-size:11px;color:#666;margin-top:4px">Select a playlist to play again</div>' +
978
+ '</div>';
979
+ };
980
+
981
+ // Highlight active playlist button
982
+ var allBtns = selectorDiv.querySelectorAll('button[data-pl-id]');
983
+ for (var bi = 0; bi < allBtns.length; bi++) {
984
+ allBtns[bi].style.background = '#222';
985
+ allBtns[bi].style.borderColor = '#444';
986
+ }
987
+ if (btnEl) { btnEl.style.background = '#ff448833'; btnEl.style.borderColor = '#ff4488'; }
988
+ }
989
+
990
+ for (var pi = 0; pi < JUKEBOX_PLAYLISTS.length; pi++) {
991
+ var pl = JUKEBOX_PLAYLISTS[pi];
992
+ var plBtn = document.createElement('button');
993
+ plBtn.textContent = '\u266B ' + pl.name;
994
+ plBtn.dataset.plId = pl.id;
995
+ plBtn.dataset.plName = pl.name;
996
+ plBtn.style.cssText = 'flex:1;min-width:80px;padding:8px;background:#222;border:1px solid #444;border-radius:8px;color:#e6edf3;font-size:11px;cursor:pointer;font-weight:500;transition:all 0.15s;';
997
+ plBtn.addEventListener('mouseenter', function() { this.style.background = '#ff448822'; });
998
+ plBtn.addEventListener('mouseleave', function() {
999
+ if (this.style.borderColor !== 'rgb(255, 68, 136)') this.style.background = '#222';
1000
+ });
1001
+ plBtn.addEventListener('click', function() { playPlaylist(this.dataset.plId, this.dataset.plName, this); });
1002
+ selectorDiv.appendChild(plBtn);
1003
+ }
1004
+
1005
+ panel.appendChild(selectorDiv);
1006
+ panel.appendChild(playerArea);
1007
+
1008
+ // Add pulse animation for now-playing icon
1009
+ var styleTag = document.createElement('style');
1010
+ styleTag.textContent = '@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}';
1011
+ panel.appendChild(styleTag);
1012
+
1013
+ // Channel link + hint
1014
+ var hint = document.createElement('div');
1015
+ hint.style.cssText = 'color:#888;font-size:10px;text-align:center;margin-top:8px;';
1016
+ hint.innerHTML = 'Music by <a href="https://www.youtube.com/@AtmosphereBeatMusic" target="_blank" style="color:#ff4488;text-decoration:none">@AtmosphereBeatMusic</a> &bull; Escape to close (music keeps playing)';
1017
+ panel.appendChild(hint);
1018
+
1019
+ overlay.appendChild(panel);
1020
+ overlay.addEventListener('click', function(e) { if (e.target === overlay) dismissJukebox(); });
1021
+ document.body.appendChild(overlay);
1022
+ _jukeboxOverlay = overlay;
1023
+
1024
+ // Update jukebox label
1025
+ if (S._jukebox) {
1026
+ S._jukebox.playing = true;
1027
+ S._jukebox.label.innerHTML = '<div style="color:#ffdd44;font-size:10px">NOW PLAYING</div><div style="font-size:7px;color:#ff4488">Walk away to close</div>';
1028
+ }
1029
+
1030
+ // Escape key to close
1031
+ var escHandler = function(e) {
1032
+ if (e.key === 'Escape') { dismissJukebox(); document.removeEventListener('keydown', escHandler); }
1033
+ };
1034
+ document.addEventListener('keydown', escHandler);
1035
+ };
1036
+
1037
+ function dismissJukebox() {
1038
+ if (_jukeboxOverlay) {
1039
+ _jukeboxOverlay.remove();
1040
+ _jukeboxOverlay = null;
1041
+ }
1042
+ if (S._jukebox) {
1043
+ S._jukebox.playing = false;
1044
+ S._jukebox.label.innerHTML = '<div style="color:#ffdd44;font-size:10px">JUKEBOX</div><div style="font-size:7px;color:#aaa">Press E to play</div>';
1045
+ }
1046
+ }