let-them-talk 4.2.0 → 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/CHANGELOG.md +42 -0
- package/cli.js +1 -1
- package/dashboard.html +193 -0
- package/dashboard.js +161 -0
- package/office/agents.js +148 -4
- package/office/animation.js +68 -0
- package/office/assets.js +431 -0
- package/office/builder.js +355 -0
- package/office/campus-env.js +119 -23
- package/office/face.js +65 -0
- package/office/index.js +623 -0
- package/office/player.js +228 -6
- package/office/world-save.js +91 -0
- package/package.json +1 -1
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, '<').replace(/>/g, '>') + (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, '<') + '</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 = '⛶'; // ⛶ 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 = '✖'; // ✖ 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 = '⛶'; // ⛶ 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> Move</div>' +
|
|
630
|
+
'<div><span style="color:#7ee787">Space</span> Jump</div>' +
|
|
631
|
+
'<div><span style="color:#7ee787">Shift</span> Sprint</div>' +
|
|
632
|
+
'<div><span style="color:#7ee787">E</span> Sit / Stand</div>' +
|
|
633
|
+
'<div><span style="color:#7ee787">Mouse</span> Look around</div>' +
|
|
634
|
+
'<div><span style="color:#7ee787">Scroll</span> Zoom in/out</div>' +
|
|
635
|
+
'<div><span style="color:#7ee787">Click</span> Agent commands</div>' +
|
|
636
|
+
'<div style="margin-top:6px;border-top:1px solid #30363d;padding-top:6px">' +
|
|
637
|
+
'<span style="color:#d2a8ff">H</span> Toggle this HUD</div>' +
|
|
638
|
+
'<div><span style="color:#d2a8ff">End</span> 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> • 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
|
+
}
|