watercooler 0.0.4 → 0.0.6

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/public/app.js CHANGED
@@ -2,19 +2,20 @@ import * as THREE from 'three';
2
2
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
3
 
4
4
  // State
5
- let config = { user: '', mailbox: '' };
5
+ let config = { user: '', mailbox: '', avatar: null };
6
6
  let messages = []; // Messages TO user (for main panel)
7
- let allMessages = []; // All messages involving user (for house dialogs)
7
+ let allMessages = []; // All messages involving user (for desk dialogs)
8
8
  let recipients = [];
9
+ let avatarStates = {}; // Map of name -> {tool_name, timestamp}
9
10
  let scene, camera, renderer, controls;
10
11
  let agentMeshes = new Map();
11
12
  let connectionLines = [];
12
13
  let raycaster, mouse;
13
14
 
14
- // Color palette for agents
15
+ // Color palette for agents - modern muted tones
15
16
  const agentColors = [
16
- 0xFF6B6B, 0x4ECDC4, 0x45B7D1, 0xFFA07A, 0x98D8C8,
17
- 0xF7DC6F, 0xBB8FCE, 0x85C1E2, 0xF8B500, 0x6C5CE7
17
+ 0x5EEAD4, 0x6EE7B7, 0x7DD3FC, 0xA78BFA, 0xFBBF24,
18
+ 0xF9A8D4, 0x86EFAC, 0x93C5FD, 0xC4B5FD, 0x67E8F9
18
19
  ];
19
20
 
20
21
  function getAgentColor(name) {
@@ -25,81 +26,107 @@ function getAgentColor(name) {
25
26
  return agentColors[Math.abs(hash) % agentColors.length];
26
27
  }
27
28
 
29
+ // Platform dimensions
30
+ const PLATFORM_SIZE = 60;
31
+ const PLATFORM_HEIGHT = 2;
32
+ const WALL_HEIGHT = 18;
33
+
34
+ // Animated objects
35
+ let holoSphere = null;
36
+ let holoParticles = null;
37
+ let glowLights = [];
38
+ let floatingParticles = [];
39
+
28
40
  // Initialize Three.js
29
41
  function init() {
30
42
  const container = document.getElementById('canvas-container');
31
43
 
32
44
  scene = new THREE.Scene();
33
- scene.background = new THREE.Color(0x667eea);
45
+ scene.background = new THREE.Color(0x1a3a3a);
46
+ scene.fog = new THREE.FogExp2(0x1a3a3a, 0.003);
34
47
 
35
- camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
36
- camera.position.set(0, 30, 40);
48
+ camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
49
+ camera.position.set(55, 45, 55);
37
50
 
38
51
  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
39
52
  renderer.setSize(window.innerWidth, window.innerHeight);
53
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
40
54
  renderer.shadowMap.enabled = true;
41
55
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
56
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
57
+ renderer.toneMappingExposure = 1.2;
42
58
  container.appendChild(renderer.domElement);
43
59
 
44
60
  controls = new OrbitControls(camera, renderer.domElement);
45
61
  controls.enableDamping = true;
46
62
  controls.dampingFactor = 0.05;
47
- controls.maxPolarAngle = Math.PI / 2 - 0.1;
48
- controls.minDistance = 20;
49
- controls.maxDistance = 80;
63
+ controls.maxPolarAngle = Math.PI / 2 - 0.05;
64
+ controls.minDistance = 25;
65
+ controls.maxDistance = 120;
50
66
  controls.enableZoom = true;
51
67
  controls.zoomSpeed = 0.8;
52
- controls.enablePan = false; // Disable pan on touch for better mobile UX
68
+ controls.enablePan = false;
69
+ controls.target.set(0, 5, 0);
53
70
  controls.touches = {
54
71
  ONE: THREE.TOUCH.ROTATE,
55
72
  TWO: THREE.TOUCH.DOLLY_PAN
56
73
  };
57
74
 
58
- // Lighting
59
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
75
+ // === Lighting ===
76
+ // Soft ambient
77
+ const ambientLight = new THREE.AmbientLight(0x2d5a5a, 0.8);
60
78
  scene.add(ambientLight);
61
79
 
62
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
63
- dirLight.position.set(50, 100, 50);
80
+ // Main directional light (warm)
81
+ const dirLight = new THREE.DirectionalLight(0xfff5e6, 0.6);
82
+ dirLight.position.set(40, 80, 30);
64
83
  dirLight.castShadow = true;
65
- dirLight.shadow.camera.left = -50;
66
- dirLight.shadow.camera.right = 50;
67
- dirLight.shadow.camera.top = 50;
68
- dirLight.shadow.camera.bottom = -50;
84
+ dirLight.shadow.camera.left = -40;
85
+ dirLight.shadow.camera.right = 40;
86
+ dirLight.shadow.camera.top = 40;
87
+ dirLight.shadow.camera.bottom = -40;
69
88
  dirLight.shadow.mapSize.width = 2048;
70
89
  dirLight.shadow.mapSize.height = 2048;
90
+ dirLight.shadow.bias = -0.001;
71
91
  scene.add(dirLight);
72
92
 
73
- // Ground
74
- const groundGeo = new THREE.PlaneGeometry(200, 200);
75
- const groundMat = new THREE.MeshStandardMaterial({
76
- color: 0x7dd3c0,
77
- roughness: 0.8
78
- });
79
- const ground = new THREE.Mesh(groundGeo, groundMat);
80
- ground.rotation.x = -Math.PI / 2;
81
- ground.receiveShadow = true;
82
- scene.add(ground);
93
+ // Fill light from below (teal tint)
94
+ const fillLight = new THREE.DirectionalLight(0x4fd1c5, 0.3);
95
+ fillLight.position.set(-20, 5, -20);
96
+ scene.add(fillLight);
97
+
98
+ // Hemisphere light for natural ambient
99
+ const hemiLight = new THREE.HemisphereLight(0x4fd1c5, 0x1a3a3a, 0.4);
100
+ scene.add(hemiLight);
101
+
102
+ // === Platform ===
103
+ createPlatform();
104
+
105
+ // === Glass Walls ===
106
+ createGlassWalls();
107
+
108
+ // === Decorative Plants ===
109
+ createPlants();
83
110
 
84
- // Grid helper
85
- const grid = new THREE.GridHelper(200, 50, 0xffffff, 0xffffff);
86
- grid.material.opacity = 0.2;
87
- grid.material.transparent = true;
88
- scene.add(grid);
111
+ // === Holographic Sphere ===
112
+ createHolographicSphere();
89
113
 
90
- // Trees
91
- createTrees();
114
+ // === Ambient Glow Lights ===
115
+ createGlowLights();
116
+
117
+ // === Floating Particles ===
118
+ createFloatingParticles();
92
119
 
93
120
  window.addEventListener('resize', onWindowResize);
94
121
 
95
- // Raycaster for house clicks
122
+ // Raycaster for desk clicks
96
123
  raycaster = new THREE.Raycaster();
97
124
  mouse = new THREE.Vector2();
98
- renderer.domElement.addEventListener('click', onHouseClick);
125
+ renderer.domElement.addEventListener('click', onDeskClick);
99
126
 
100
127
  // Add touch support for mobile
101
- renderer.domElement.addEventListener('touchstart', onHouseTouchStart, { passive: false });
102
- renderer.domElement.addEventListener('touchend', onHouseTouchEnd, { passive: false });
128
+ renderer.domElement.addEventListener('touchstart', onDeskTouchStart, { passive: false });
129
+ renderer.domElement.addEventListener('touchend', onDeskTouchEnd, { passive: false });
103
130
 
104
131
  // Disable context menu on mobile for better UX
105
132
  renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
@@ -112,147 +139,648 @@ function init() {
112
139
  animate();
113
140
  }
114
141
 
115
- function createTrees() {
116
- for (let i = 0; i < 30; i++) {
117
- const x = (Math.random() - 0.5) * 150;
118
- const z = (Math.random() - 0.5) * 150;
119
-
120
- // Don't place trees too close to center
121
- if (Math.sqrt(x*x + z*z) < 30) continue;
122
-
123
- const trunkGeo = new THREE.CylinderGeometry(0.5, 0.8, 3, 8);
124
- const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
125
- const trunk = new THREE.Mesh(trunkGeo, trunkMat);
126
- trunk.position.set(x, 1.5, z);
127
- trunk.castShadow = true;
142
+ function createPlatform() {
143
+ // Main platform - dark concrete slab
144
+ const platformGeo = new THREE.BoxGeometry(PLATFORM_SIZE, PLATFORM_HEIGHT, PLATFORM_SIZE);
145
+ const platformMat = new THREE.MeshStandardMaterial({
146
+ color: 0x3a3a3a,
147
+ roughness: 0.4,
148
+ metalness: 0.1
149
+ });
150
+ const platform = new THREE.Mesh(platformGeo, platformMat);
151
+ platform.position.y = PLATFORM_HEIGHT / 2;
152
+ platform.receiveShadow = true;
153
+ platform.castShadow = true;
154
+ scene.add(platform);
155
+
156
+ // Edge trim - lighter accent
157
+ const trimGeo = new THREE.BoxGeometry(PLATFORM_SIZE + 0.5, 0.3, PLATFORM_SIZE + 0.5);
158
+ const trimMat = new THREE.MeshStandardMaterial({
159
+ color: 0x5a5a5a,
160
+ roughness: 0.3,
161
+ metalness: 0.3
162
+ });
163
+ const trim = new THREE.Mesh(trimGeo, trimMat);
164
+ trim.position.y = PLATFORM_HEIGHT + 0.15;
165
+ scene.add(trim);
166
+
167
+ // Floor surface - polished concrete with subtle grid
168
+ const floorGeo = new THREE.PlaneGeometry(PLATFORM_SIZE - 2, PLATFORM_SIZE - 2);
169
+ const floorMat = new THREE.MeshStandardMaterial({
170
+ color: 0x4a4a4a,
171
+ roughness: 0.2,
172
+ metalness: 0.15
173
+ });
174
+ const floor = new THREE.Mesh(floorGeo, floorMat);
175
+ floor.rotation.x = -Math.PI / 2;
176
+ floor.position.y = PLATFORM_HEIGHT + 0.02;
177
+ floor.receiveShadow = true;
178
+ scene.add(floor);
179
+
180
+ // Subtle grid on floor
181
+ const gridHelper = new THREE.GridHelper(PLATFORM_SIZE - 4, 20, 0x555555, 0x444444);
182
+ gridHelper.position.y = PLATFORM_HEIGHT + 0.05;
183
+ gridHelper.material.opacity = 0.15;
184
+ gridHelper.material.transparent = true;
185
+ scene.add(gridHelper);
186
+
187
+ // Ground below platform (dark reflection surface)
188
+ const groundGeo = new THREE.PlaneGeometry(300, 300);
189
+ const groundMat = new THREE.MeshStandardMaterial({
190
+ color: 0x1a3a3a,
191
+ roughness: 0.6,
192
+ metalness: 0.2
193
+ });
194
+ const ground = new THREE.Mesh(groundGeo, groundMat);
195
+ ground.rotation.x = -Math.PI / 2;
196
+ ground.position.y = -0.1;
197
+ ground.receiveShadow = true;
198
+ scene.add(ground);
199
+ }
200
+
201
+ function createGlassWalls() {
202
+ const glassMat = new THREE.MeshPhysicalMaterial({
203
+ color: 0x88cccc,
204
+ transparent: true,
205
+ opacity: 0.08,
206
+ roughness: 0.05,
207
+ metalness: 0.0,
208
+ transmission: 0.95,
209
+ thickness: 0.5,
210
+ side: THREE.DoubleSide
211
+ });
212
+
213
+ const wallHeight = WALL_HEIGHT;
214
+ const wallY = PLATFORM_HEIGHT + wallHeight / 2;
215
+ const halfSize = PLATFORM_SIZE / 2;
216
+
217
+ // Back wall
218
+ const backWall = new THREE.Mesh(
219
+ new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
220
+ glassMat
221
+ );
222
+ backWall.position.set(0, wallY, -halfSize);
223
+ scene.add(backWall);
224
+
225
+ // Left wall
226
+ const leftWall = new THREE.Mesh(
227
+ new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
228
+ glassMat
229
+ );
230
+ leftWall.position.set(-halfSize, wallY, 0);
231
+ leftWall.rotation.y = Math.PI / 2;
232
+ scene.add(leftWall);
233
+
234
+ // Right wall (partial, for openness)
235
+ const rightWall = new THREE.Mesh(
236
+ new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
237
+ glassMat
238
+ );
239
+ rightWall.position.set(halfSize, wallY, 0);
240
+ rightWall.rotation.y = -Math.PI / 2;
241
+ scene.add(rightWall);
242
+
243
+ // Glass edge frames (vertical pillars at corners)
244
+ const pillarGeo = new THREE.BoxGeometry(0.5, wallHeight, 0.5);
245
+ const pillarMat = new THREE.MeshStandardMaterial({
246
+ color: 0x777777,
247
+ roughness: 0.2,
248
+ metalness: 0.6
249
+ });
250
+
251
+ const corners = [
252
+ [-halfSize, wallY, -halfSize],
253
+ [halfSize, wallY, -halfSize],
254
+ [-halfSize, wallY, halfSize],
255
+ [halfSize, wallY, halfSize]
256
+ ];
257
+
258
+ corners.forEach(pos => {
259
+ const pillar = new THREE.Mesh(pillarGeo, pillarMat);
260
+ pillar.position.set(...pos);
261
+ pillar.castShadow = true;
262
+ scene.add(pillar);
263
+ });
264
+
265
+ // Top edge frame
266
+ const topFrameMat = new THREE.MeshStandardMaterial({
267
+ color: 0x666666,
268
+ roughness: 0.2,
269
+ metalness: 0.5
270
+ });
271
+
272
+ const frameY = PLATFORM_HEIGHT + wallHeight;
273
+
274
+ // Back top frame
275
+ const backFrame = new THREE.Mesh(new THREE.BoxGeometry(PLATFORM_SIZE, 0.3, 0.3), topFrameMat);
276
+ backFrame.position.set(0, frameY, -halfSize);
277
+ scene.add(backFrame);
278
+
279
+ // Left top frame
280
+ const leftFrame = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, PLATFORM_SIZE), topFrameMat);
281
+ leftFrame.position.set(-halfSize, frameY, 0);
282
+ scene.add(leftFrame);
283
+
284
+ // Right top frame
285
+ const rightFrame = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, PLATFORM_SIZE), topFrameMat);
286
+ rightFrame.position.set(halfSize, frameY, 0);
287
+ scene.add(rightFrame);
288
+ }
289
+
290
+ function createPlants() {
291
+ const plantPositions = [
292
+ // Corner clusters
293
+ [-25, PLATFORM_HEIGHT, -25],
294
+ [25, PLATFORM_HEIGHT, -25],
295
+ [-25, PLATFORM_HEIGHT, 25],
296
+ [25, PLATFORM_HEIGHT, 25],
297
+ // Edge accents
298
+ [-20, PLATFORM_HEIGHT, 27],
299
+ [20, PLATFORM_HEIGHT, 27],
300
+ [-27, PLATFORM_HEIGHT, 0],
301
+ [27, PLATFORM_HEIGHT, -15],
302
+ ];
303
+
304
+ plantPositions.forEach(pos => {
305
+ createPlantCluster(pos[0], pos[1], pos[2]);
306
+ });
307
+ }
308
+
309
+ function createPlantCluster(x, y, z) {
310
+ const group = new THREE.Group();
311
+
312
+ // Planter box
313
+ const planterGeo = new THREE.BoxGeometry(3, 1.5, 3);
314
+ const planterMat = new THREE.MeshStandardMaterial({
315
+ color: 0x2a2a2a,
316
+ roughness: 0.6,
317
+ metalness: 0.1
318
+ });
319
+ const planter = new THREE.Mesh(planterGeo, planterMat);
320
+ planter.position.y = 0.75;
321
+ planter.castShadow = true;
322
+ planter.receiveShadow = true;
323
+ group.add(planter);
324
+
325
+ // Soil
326
+ const soilGeo = new THREE.BoxGeometry(2.6, 0.2, 2.6);
327
+ const soilMat = new THREE.MeshStandardMaterial({ color: 0x3d2817 });
328
+ const soil = new THREE.Mesh(soilGeo, soilMat);
329
+ soil.position.y = 1.5;
330
+ group.add(soil);
331
+
332
+ // Foliage - multiple spheres for bush look
333
+ const leafColors = [0x1a6b3a, 0x228B22, 0x2d8b4e, 0x1f7a3f];
334
+
335
+ for (let i = 0; i < 5; i++) {
336
+ const size = 0.6 + Math.random() * 0.8;
337
+ const leafGeo = new THREE.SphereGeometry(size, 8, 8);
338
+ const leafMat = new THREE.MeshStandardMaterial({
339
+ color: leafColors[Math.floor(Math.random() * leafColors.length)],
340
+ roughness: 0.8
341
+ });
342
+ const leaf = new THREE.Mesh(leafGeo, leafMat);
343
+ leaf.position.set(
344
+ (Math.random() - 0.5) * 1.5,
345
+ 1.8 + Math.random() * 1.5,
346
+ (Math.random() - 0.5) * 1.5
347
+ );
348
+ leaf.castShadow = true;
349
+ group.add(leaf);
350
+ }
351
+
352
+ // Tall fern-like elements (cone shapes)
353
+ for (let i = 0; i < 3; i++) {
354
+ const fernGeo = new THREE.ConeGeometry(0.3, 2 + Math.random() * 2, 6);
355
+ const fernMat = new THREE.MeshStandardMaterial({
356
+ color: 0x1a5c2e,
357
+ roughness: 0.7
358
+ });
359
+ const fern = new THREE.Mesh(fernGeo, fernMat);
360
+ fern.position.set(
361
+ (Math.random() - 0.5) * 1.5,
362
+ 2.5 + Math.random() * 1.5,
363
+ (Math.random() - 0.5) * 1.5
364
+ );
365
+ fern.castShadow = true;
366
+ group.add(fern);
367
+ }
368
+
369
+ group.position.set(x, y, z);
370
+ scene.add(group);
371
+ }
372
+
373
+ function createHolographicSphere() {
374
+ // Wireframe sphere
375
+ const sphereGeo = new THREE.IcosahedronGeometry(6, 3);
376
+ const sphereMat = new THREE.MeshBasicMaterial({
377
+ color: 0x4fd1c5,
378
+ wireframe: true,
379
+ transparent: true,
380
+ opacity: 0.3
381
+ });
382
+ holoSphere = new THREE.Mesh(sphereGeo, sphereMat);
383
+ holoSphere.position.set(0, PLATFORM_HEIGHT + 12, 0);
384
+ scene.add(holoSphere);
385
+
386
+ // Inner glow sphere
387
+ const innerGeo = new THREE.SphereGeometry(4, 32, 32);
388
+ const innerMat = new THREE.MeshBasicMaterial({
389
+ color: 0x4fd1c5,
390
+ transparent: true,
391
+ opacity: 0.05
392
+ });
393
+ const innerSphere = new THREE.Mesh(innerGeo, innerMat);
394
+ holoSphere.add(innerSphere);
395
+
396
+ // Point cloud on sphere surface
397
+ const particleCount = 300;
398
+ const positions = new Float32Array(particleCount * 3);
399
+ for (let i = 0; i < particleCount; i++) {
400
+ const theta = Math.random() * Math.PI * 2;
401
+ const phi = Math.acos(2 * Math.random() - 1);
402
+ const r = 5.5 + Math.random() * 0.5;
403
+ positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
404
+ positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
405
+ positions[i * 3 + 2] = r * Math.cos(phi);
406
+ }
407
+
408
+ const particleGeo = new THREE.BufferGeometry();
409
+ particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
410
+ const particleMat = new THREE.PointsMaterial({
411
+ color: 0x88ffee,
412
+ size: 0.15,
413
+ transparent: true,
414
+ opacity: 0.6,
415
+ blending: THREE.AdditiveBlending
416
+ });
417
+ holoParticles = new THREE.Points(particleGeo, particleMat);
418
+ holoSphere.add(holoParticles);
419
+
420
+ // Point light from the sphere
421
+ const sphereLight = new THREE.PointLight(0x4fd1c5, 0.8, 35);
422
+ sphereLight.position.copy(holoSphere.position);
423
+ scene.add(sphereLight);
424
+ }
425
+
426
+ function createGlowLights() {
427
+ // Floor-standing lamp posts
428
+ const lampPositions = [
429
+ [20, PLATFORM_HEIGHT, 15],
430
+ [-20, PLATFORM_HEIGHT, 15],
431
+ [20, PLATFORM_HEIGHT, -20],
432
+ [-20, PLATFORM_HEIGHT, -20],
433
+ ];
434
+
435
+ lampPositions.forEach(pos => {
436
+ // Lamp post
437
+ const postGeo = new THREE.CylinderGeometry(0.15, 0.15, 6, 8);
438
+ const postMat = new THREE.MeshStandardMaterial({
439
+ color: 0x555555,
440
+ roughness: 0.3,
441
+ metalness: 0.7
442
+ });
443
+ const post = new THREE.Mesh(postGeo, postMat);
444
+ post.position.set(pos[0], pos[1] + 3, pos[2]);
445
+ post.castShadow = true;
446
+ scene.add(post);
128
447
 
129
- const leavesGeo = new THREE.ConeGeometry(3, 8, 8);
130
- const leavesMat = new THREE.MeshStandardMaterial({ color: 0x228B22 });
131
- const leaves = new THREE.Mesh(leavesGeo, leavesMat);
132
- leaves.position.set(x, 6, z);
133
- leaves.castShadow = true;
448
+ // Lamp bulb
449
+ const bulbGeo = new THREE.SphereGeometry(0.4, 16, 16);
450
+ const bulbMat = new THREE.MeshBasicMaterial({
451
+ color: 0xffcc66,
452
+ transparent: true,
453
+ opacity: 0.9
454
+ });
455
+ const bulb = new THREE.Mesh(bulbGeo, bulbMat);
456
+ bulb.position.set(pos[0], pos[1] + 6.2, pos[2]);
457
+ scene.add(bulb);
134
458
 
135
- scene.add(trunk);
136
- scene.add(leaves);
459
+ // Point light
460
+ const light = new THREE.PointLight(0xffcc66, 0.5, 18);
461
+ light.position.set(pos[0], pos[1] + 6.2, pos[2]);
462
+ light.castShadow = false;
463
+ scene.add(light);
464
+ glowLights.push({ bulb, light, baseIntensity: 0.5 });
465
+ });
466
+ }
467
+
468
+ function createFloatingParticles() {
469
+ const particleCount = 80;
470
+ const positions = new Float32Array(particleCount * 3);
471
+
472
+ for (let i = 0; i < particleCount; i++) {
473
+ positions[i * 3] = (Math.random() - 0.5) * PLATFORM_SIZE;
474
+ positions[i * 3 + 1] = PLATFORM_HEIGHT + 2 + Math.random() * WALL_HEIGHT;
475
+ positions[i * 3 + 2] = (Math.random() - 0.5) * PLATFORM_SIZE;
137
476
  }
477
+
478
+ const geometry = new THREE.BufferGeometry();
479
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
480
+
481
+ const material = new THREE.PointsMaterial({
482
+ color: 0x88ffdd,
483
+ size: 0.12,
484
+ transparent: true,
485
+ opacity: 0.4,
486
+ blending: THREE.AdditiveBlending
487
+ });
488
+
489
+ const particles = new THREE.Points(geometry, material);
490
+ scene.add(particles);
491
+ floatingParticles.push(particles);
138
492
  }
139
493
 
140
- function createAgentHouse(name, position) {
494
+ function createAgentDesk(name, position, toolName = null) {
141
495
  const color = getAgentColor(name);
142
496
  const group = new THREE.Group();
143
497
  group.position.copy(position);
498
+ group.position.y = PLATFORM_HEIGHT;
499
+
500
+ // Modern desk - white top with thin legs
501
+ const deskTopGeo = new THREE.BoxGeometry(5, 0.2, 3);
502
+ const deskMat = new THREE.MeshStandardMaterial({
503
+ color: 0xe8e8e8,
504
+ roughness: 0.3,
505
+ metalness: 0.1
506
+ });
507
+ const deskTop = new THREE.Mesh(deskTopGeo, deskMat);
508
+ deskTop.position.y = 2.5;
509
+ deskTop.castShadow = true;
510
+ deskTop.receiveShadow = true;
511
+ group.add(deskTop);
512
+
513
+ // Desk legs - thin metal
514
+ const legGeo = new THREE.CylinderGeometry(0.08, 0.08, 2.4, 8);
515
+ const legMat = new THREE.MeshStandardMaterial({
516
+ color: 0x999999,
517
+ roughness: 0.2,
518
+ metalness: 0.7
519
+ });
520
+ const legPositions = [
521
+ [-2.2, 1.2, -1.2],
522
+ [2.2, 1.2, -1.2],
523
+ [-2.2, 1.2, 1.2],
524
+ [2.2, 1.2, 1.2]
525
+ ];
526
+ legPositions.forEach(pos => {
527
+ const leg = new THREE.Mesh(legGeo, legMat);
528
+ leg.position.set(...pos);
529
+ group.add(leg);
530
+ });
531
+
532
+ // Modern chair - sleek
533
+ const chairSeatGeo = new THREE.BoxGeometry(1.8, 0.15, 1.8);
534
+ const chairMat = new THREE.MeshStandardMaterial({
535
+ color: 0x2a2a2a,
536
+ roughness: 0.5,
537
+ metalness: 0.2
538
+ });
539
+ const chairSeat = new THREE.Mesh(chairSeatGeo, chairMat);
540
+ chairSeat.position.set(0, 1.6, 3.2);
541
+ chairSeat.castShadow = true;
542
+ group.add(chairSeat);
543
+
544
+ // Chair back - curved look (box approximation)
545
+ const chairBackGeo = new THREE.BoxGeometry(1.8, 2.2, 0.15);
546
+ const chairBack = new THREE.Mesh(chairBackGeo, chairMat);
547
+ chairBack.position.set(0, 2.7, 4.1);
548
+ chairBack.castShadow = true;
549
+ group.add(chairBack);
550
+
551
+ // Chair post
552
+ const chairPostGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.2, 8);
553
+ const chairPost = new THREE.Mesh(chairPostGeo, legMat);
554
+ chairPost.position.set(0, 0.9, 3.2);
555
+ group.add(chairPost);
556
+
557
+ // Chair base star
558
+ for (let i = 0; i < 5; i++) {
559
+ const armGeo = new THREE.CylinderGeometry(0.06, 0.06, 1.2, 6);
560
+ const arm = new THREE.Mesh(armGeo, legMat);
561
+ const angle = (i / 5) * Math.PI * 2;
562
+ arm.rotation.z = Math.PI / 2;
563
+ arm.position.set(
564
+ Math.cos(angle) * 0.5,
565
+ 0.3,
566
+ 3.2 + Math.sin(angle) * 0.5
567
+ );
568
+ arm.rotation.y = angle;
569
+ group.add(arm);
570
+ }
144
571
 
145
- // House base
146
- const baseGeo = new THREE.BoxGeometry(6, 4, 6);
147
- const baseMat = new THREE.MeshStandardMaterial({ color: color });
148
- const base = new THREE.Mesh(baseGeo, baseMat);
149
- base.position.y = 2;
150
- base.castShadow = true;
151
- base.receiveShadow = true;
152
- group.add(base);
153
-
154
- // Roof
155
- const roofGeo = new THREE.ConeGeometry(5, 3, 4);
156
- const roofMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
157
- const roof = new THREE.Mesh(roofGeo, roofMat);
158
- roof.position.y = 5.5;
159
- roof.rotation.y = Math.PI / 4;
160
- roof.castShadow = true;
161
- group.add(roof);
162
-
163
- // Door
164
- const doorGeo = new THREE.BoxGeometry(1.5, 2.5, 0.2);
165
- const doorMat = new THREE.MeshStandardMaterial({ color: 0x4a3c28 });
166
- const door = new THREE.Mesh(doorGeo, doorMat);
167
- door.position.set(0, 1.25, 3.1);
168
- group.add(door);
169
-
170
- // Windows
171
- const windowGeo = new THREE.BoxGeometry(1.2, 1.2, 0.2);
172
- const windowMat = new THREE.MeshStandardMaterial({
173
- color: 0xFFFF99,
174
- emissive: 0xFFFF99,
175
- emissiveIntensity: 0.3
176
- });
177
-
178
- const window1 = new THREE.Mesh(windowGeo, windowMat);
179
- window1.position.set(-1.8, 2.5, 3.1);
180
- group.add(window1);
181
-
182
- const window2 = new THREE.Mesh(windowGeo, windowMat);
183
- window2.position.set(1.8, 2.5, 3.1);
184
- group.add(window2);
572
+ // Person - Body (sitting, modern look)
573
+ const bodyGeo = new THREE.CylinderGeometry(0.6, 0.5, 2, 8);
574
+ const bodyMat = new THREE.MeshStandardMaterial({
575
+ color: color,
576
+ roughness: 0.6,
577
+ metalness: 0.05
578
+ });
579
+ const body = new THREE.Mesh(bodyGeo, bodyMat);
580
+ body.position.set(0, 2.7, 3.2);
581
+ body.castShadow = true;
582
+ group.add(body);
583
+
584
+ // Person - Head
585
+ const headGeo = new THREE.SphereGeometry(0.5, 16, 16);
586
+ const headMat = new THREE.MeshStandardMaterial({
587
+ color: 0xf5d0b0,
588
+ roughness: 0.7
589
+ });
590
+ const head = new THREE.Mesh(headGeo, headMat);
591
+ head.position.set(0, 4.0, 3.2);
592
+ head.castShadow = true;
593
+ group.add(head);
594
+
595
+ // Person - Arms on desk
596
+ const armObjGeo = new THREE.CylinderGeometry(0.12, 0.12, 1.8, 6);
597
+ const armMat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.6 });
598
+
599
+ const leftArm = new THREE.Mesh(armObjGeo, armMat);
600
+ leftArm.rotation.z = Math.PI / 2;
601
+ leftArm.rotation.y = 0.3;
602
+ leftArm.position.set(-0.8, 2.8, 2);
603
+ group.add(leftArm);
604
+
605
+ const rightArm = new THREE.Mesh(armObjGeo, armMat);
606
+ rightArm.rotation.z = Math.PI / 2;
607
+ rightArm.rotation.y = -0.3;
608
+ rightArm.position.set(0.8, 2.8, 2);
609
+ group.add(rightArm);
610
+
611
+ // Monitor (modern flat screen)
612
+ const monitorStandGeo = new THREE.CylinderGeometry(0.5, 0.6, 0.1, 16);
613
+ const monitorMat = new THREE.MeshStandardMaterial({
614
+ color: 0x333333,
615
+ roughness: 0.3,
616
+ metalness: 0.5
617
+ });
618
+ const monitorStand = new THREE.Mesh(monitorStandGeo, monitorMat);
619
+ monitorStand.position.set(0, 2.65, 0.8);
620
+ group.add(monitorStand);
621
+
622
+ const monitorNeckGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.2, 8);
623
+ const monitorNeck = new THREE.Mesh(monitorNeckGeo, monitorMat);
624
+ monitorNeck.position.set(0, 3.2, 0.8);
625
+ group.add(monitorNeck);
626
+
627
+ // Screen
628
+ const screenFrameGeo = new THREE.BoxGeometry(3, 1.8, 0.12);
629
+ const screenFrame = new THREE.Mesh(screenFrameGeo, monitorMat);
630
+ screenFrame.position.set(0, 4.0, 0.8);
631
+ screenFrame.castShadow = true;
632
+ group.add(screenFrame);
633
+
634
+ // Screen display (glowing)
635
+ const screenDisplayGeo = new THREE.PlaneGeometry(2.7, 1.5);
636
+ const screenDisplayMat = new THREE.MeshBasicMaterial({
637
+ color: 0x2a6b5e,
638
+ });
639
+ const screenDisplay = new THREE.Mesh(screenDisplayGeo, screenDisplayMat);
640
+ screenDisplay.position.set(0, 4.0, 0.87);
641
+ group.add(screenDisplay);
642
+
643
+ // Screen glow light
644
+ const screenLight = new THREE.PointLight(0x4fd1c5, 0.3, 6);
645
+ screenLight.position.set(0, 4.0, 1.5);
646
+ group.add(screenLight);
647
+
648
+ // Keyboard
649
+ const kbGeo = new THREE.BoxGeometry(1.6, 0.05, 0.5);
650
+ const kbMat = new THREE.MeshStandardMaterial({
651
+ color: 0x444444,
652
+ roughness: 0.5,
653
+ metalness: 0.3
654
+ });
655
+ const keyboard = new THREE.Mesh(kbGeo, kbMat);
656
+ keyboard.position.set(0, 2.63, 2);
657
+ group.add(keyboard);
185
658
 
186
659
  // Name label sprite
187
660
  const canvas = document.createElement('canvas');
188
661
  const context = canvas.getContext('2d');
189
- // High DPI canvas for crisp text
190
662
  const scale = 2;
191
663
  canvas.width = 512;
192
- canvas.height = 128;
664
+ canvas.height = toolName ? 160 : 128;
193
665
  context.scale(scale, scale);
194
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
195
- context.roundRect(0, 0, 256, 64, 16);
666
+
667
+ // Frosted glass background
668
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
669
+ context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
196
670
  context.fill();
197
- context.font = 'bold 24px Arial';
198
- context.fillStyle = 'white';
671
+
672
+ // Subtle border
673
+ context.strokeStyle = 'rgba(79, 209, 197, 0.4)';
674
+ context.lineWidth = 1;
675
+ context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
676
+ context.stroke();
677
+
678
+ context.font = 'bold 22px Arial';
679
+ context.fillStyle = '#e0f5f0';
199
680
  context.textAlign = 'center';
200
681
  context.textBaseline = 'middle';
201
- context.fillText(name, 128, 32);
682
+ context.fillText(name, 128, 24);
683
+
684
+ if (toolName) {
685
+ context.font = 'italic 14px Arial';
686
+ context.fillStyle = '#4fd1c5';
687
+ context.fillText(toolName, 128, 56);
688
+ }
202
689
 
203
690
  const texture = new THREE.CanvasTexture(canvas);
204
691
  texture.minFilter = THREE.LinearFilter;
205
692
  texture.magFilter = THREE.LinearFilter;
206
693
  const spriteMat = new THREE.SpriteMaterial({ map: texture });
207
694
  const sprite = new THREE.Sprite(spriteMat);
208
- sprite.position.set(0, 8, 0);
209
- sprite.scale.set(8, 2, 1);
695
+ sprite.position.set(0, 6.5, 2);
696
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
697
+ sprite.name = 'label';
210
698
  group.add(sprite);
211
699
 
212
- // Path to house
213
- const pathGeo = new THREE.PlaneGeometry(2, 8);
214
- const pathMat = new THREE.MeshStandardMaterial({ color: 0xD2B48C });
215
- const path = new THREE.Mesh(pathGeo, pathMat);
216
- path.rotation.x = -Math.PI / 2;
217
- path.position.set(0, 0.02, 7);
218
- group.add(path);
219
-
220
700
  scene.add(group);
221
701
  agentMeshes.set(name, group);
222
702
 
223
703
  return group;
224
704
  }
225
705
 
706
+ function updateDeskLabel(desk, name, toolName = null) {
707
+ const sprite = desk.getObjectByName('label');
708
+ if (!sprite) return;
709
+
710
+ const canvas = document.createElement('canvas');
711
+ const context = canvas.getContext('2d');
712
+ const scale = 2;
713
+ canvas.width = 512;
714
+ canvas.height = toolName ? 160 : 128;
715
+ context.scale(scale, scale);
716
+
717
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
718
+ context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
719
+ context.fill();
720
+
721
+ context.strokeStyle = 'rgba(79, 209, 197, 0.4)';
722
+ context.lineWidth = 1;
723
+ context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
724
+ context.stroke();
725
+
726
+ context.font = 'bold 22px Arial';
727
+ context.fillStyle = '#e0f5f0';
728
+ context.textAlign = 'center';
729
+ context.textBaseline = 'middle';
730
+ context.fillText(name, 128, 24);
731
+
732
+ if (toolName) {
733
+ context.font = 'italic 14px Arial';
734
+ context.fillStyle = '#4fd1c5';
735
+ context.fillText(toolName, 128, 56);
736
+ }
737
+
738
+ const texture = new THREE.CanvasTexture(canvas);
739
+ texture.minFilter = THREE.LinearFilter;
740
+ texture.magFilter = THREE.LinearFilter;
741
+ sprite.material.map = texture;
742
+ sprite.material.needsUpdate = true;
743
+
744
+ sprite.position.set(0, 6.5, 2);
745
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
746
+ }
747
+
226
748
  function createMessageParticle(fromPos, toPos) {
227
- const particleGeo = new THREE.SphereGeometry(0.3, 8, 8);
228
- const particleMat = new THREE.MeshStandardMaterial({
229
- color: 0xFFD700,
230
- emissive: 0xFFD700,
231
- emissiveIntensity: 0.5
749
+ const particleGeo = new THREE.SphereGeometry(0.35, 12, 12);
750
+ const particleMat = new THREE.MeshBasicMaterial({
751
+ color: 0xff6b6b,
752
+ transparent: true,
753
+ opacity: 0.95
232
754
  });
233
755
  const particle = new THREE.Mesh(particleGeo, particleMat);
234
756
 
235
757
  particle.position.copy(fromPos);
236
- particle.position.y += 8;
758
+ particle.position.y += 5;
759
+
760
+ // Add a glow point light that follows particle
761
+ const glow = new THREE.PointLight(0xff6b6b, 1.0, 10);
762
+ particle.add(glow);
237
763
 
238
764
  scene.add(particle);
239
765
 
240
- // Animate particle
241
766
  const startTime = Date.now();
242
- const duration = 2000;
767
+ const duration = 1500;
243
768
 
244
769
  function animateParticle() {
245
770
  const elapsed = Date.now() - startTime;
246
771
  const progress = Math.min(elapsed / duration, 1);
247
772
 
248
773
  particle.position.lerpVectors(
249
- new THREE.Vector3(fromPos.x, fromPos.y + 8, fromPos.z),
250
- new THREE.Vector3(toPos.x, toPos.y + 8, toPos.z),
774
+ new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z),
775
+ new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z),
251
776
  progress
252
777
  );
253
778
 
254
- // Add arc
255
- particle.position.y += Math.sin(progress * Math.PI) * 3;
779
+ particle.position.y += Math.sin(progress * Math.PI) * 2;
780
+
781
+ // Pulse size
782
+ const pulse = Math.sin(progress * Math.PI) * 0.2 + 1;
783
+ particle.scale.setScalar(pulse);
256
784
 
257
785
  if (progress < 1) {
258
786
  requestAnimationFrame(animateParticle);
@@ -268,48 +796,58 @@ function createConnectionLine(fromPos, toPos) {
268
796
  const startPos = new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z);
269
797
  const endPos = new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z);
270
798
 
799
+ // Create curved line with points
800
+ const mid = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5);
801
+ mid.y += 2;
802
+
803
+ const curve = new THREE.QuadraticBezierCurve3(startPos, mid, endPos);
804
+ const points = curve.getPoints(50);
805
+
806
+ // Main line - thicker, glowing red
271
807
  const material = new THREE.LineBasicMaterial({
272
808
  color: 0xff6b6b,
273
- opacity: 0.8,
809
+ opacity: 0.7,
274
810
  transparent: true,
275
811
  linewidth: 3
276
812
  });
277
813
 
278
- const points = [startPos, endPos];
279
-
280
814
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
281
815
  const line = new THREE.Line(geometry, material);
282
816
 
283
817
  scene.add(line);
284
818
  connectionLines.push(line);
285
819
 
286
- // Add arrowhead for directionality
820
+ // Add small chevron markers along the path to show direction
287
821
  const direction = new THREE.Vector3().subVectors(endPos, startPos).normalize();
288
- const arrowPos = endPos.clone().sub(direction.clone().multiplyScalar(5));
289
-
290
- const arrowGeometry = new THREE.ConeGeometry(0.5, 1.5, 8);
291
- const arrowMaterial = new THREE.MeshStandardMaterial({
292
- color: 0xff6b6b,
293
- emissive: 0xff6b6b,
294
- emissiveIntensity: 0.3
295
- });
296
- const arrowhead = new THREE.Mesh(arrowGeometry, arrowMaterial);
297
-
298
- arrowhead.position.copy(arrowPos);
299
-
300
- // Orient arrow to point along the line
301
822
  const up = new THREE.Vector3(0, 1, 0);
302
- const quaternion = new THREE.Quaternion();
303
- quaternion.setFromUnitVectors(up, direction);
304
- arrowhead.setRotationFromQuaternion(quaternion);
305
-
306
- scene.add(arrowhead);
307
- connectionLines.push(arrowhead);
823
+ const numMarkers = 4;
824
+ for (let i = 0; i < numMarkers; i++) {
825
+ const t = (i + 1) / (numMarkers + 1);
826
+ const markerPos = curve.getPoint(t);
827
+
828
+ const markerGeo = new THREE.ConeGeometry(0.25, 0.7, 8);
829
+ const markerMat = new THREE.MeshBasicMaterial({
830
+ color: 0xff6b6b,
831
+ transparent: true,
832
+ opacity: 0.5
833
+ });
834
+ const marker = new THREE.Mesh(markerGeo, markerMat);
835
+
836
+ // Get tangent at this point for direction
837
+ const tangent = curve.getTangent(t);
838
+ marker.position.copy(markerPos);
839
+
840
+ const markerQuat = new THREE.Quaternion();
841
+ markerQuat.setFromUnitVectors(up, tangent);
842
+ marker.setRotationFromQuaternion(markerQuat);
843
+
844
+ scene.add(marker);
845
+ connectionLines.push(marker);
846
+ }
308
847
 
309
- // Send particle
310
848
  setTimeout(() => {
311
849
  createMessageParticle(fromPos, toPos);
312
- }, 100);
850
+ }, 50);
313
851
  }
314
852
 
315
853
  function clearConnections() {
@@ -321,53 +859,107 @@ function updateVillage() {
321
859
  clearConnections();
322
860
 
323
861
  // Use recipients (from coworkers.db) as the authoritative list of agents
324
- // This ensures we only show houses for registered coworkers
325
862
  const allAgents = new Set([config.user.toLowerCase(), ...recipients.map(r => r.toLowerCase())]);
326
863
 
327
- // Also add message participants that might not be in coworker db yet
864
+ // Also add message participants
328
865
  messages.forEach(m => {
329
866
  allAgents.add(m.sender.toLowerCase());
330
867
  allAgents.add(m.recipient.toLowerCase());
331
868
  });
332
869
 
333
- // Arrange agents in a circle
870
+ // Arrange agents in a circle on the platform
334
871
  const agents = Array.from(allAgents);
335
- const radius = 25;
872
+ const radius = Math.min(20, Math.max(10, agents.length * 3));
336
873
 
337
874
  agents.forEach((agent, index) => {
338
- const angle = (index / agents.length) * Math.PI * 2;
875
+ const angle = (index / agents.length) * Math.PI * 2 - Math.PI / 2;
339
876
  const x = Math.cos(angle) * radius;
340
877
  const z = Math.sin(angle) * radius;
341
878
  const position = new THREE.Vector3(x, 0, z);
342
879
 
880
+ const avatarState = avatarStates[agent.toLowerCase()];
881
+ const toolName = avatarState?.tool_name || null;
882
+
343
883
  if (!agentMeshes.has(agent)) {
344
- createAgentHouse(agent, position);
884
+ const group = createAgentDesk(agent, position, toolName);
885
+ // Face desk toward center
886
+ group.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
345
887
  } else {
346
- // Update position if needed
347
- const house = agentMeshes.get(agent);
348
- house.position.copy(position);
888
+ const desk = agentMeshes.get(agent);
889
+ desk.position.set(x, PLATFORM_HEIGHT, z);
890
+ desk.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
891
+ updateDeskLabel(desk, agent, toolName);
892
+ }
893
+ });
894
+
895
+ // Remove desks for coworkers that no longer exist
896
+ agentMeshes.forEach((desk, name) => {
897
+ if (!allAgents.has(name)) {
898
+ // Remove from scene
899
+ scene.remove(desk);
900
+
901
+ // Dispose of geometries and materials to prevent memory leaks
902
+ desk.traverse((child) => {
903
+ if (child.isMesh) {
904
+ child.geometry.dispose();
905
+ if (child.material) {
906
+ if (Array.isArray(child.material)) {
907
+ child.material.forEach(m => m.dispose());
908
+ } else {
909
+ child.material.dispose();
910
+ }
911
+ }
912
+ }
913
+ });
914
+
915
+ // Remove from Map
916
+ agentMeshes.delete(name);
349
917
  }
350
918
  });
351
919
 
352
920
  // Create connections for unread messages only
353
921
  allMessages.forEach(msg => {
354
- const fromHouse = agentMeshes.get(msg.sender.toLowerCase());
355
- const toHouse = agentMeshes.get(msg.recipient.toLowerCase());
922
+ const fromDesk = agentMeshes.get(msg.sender.toLowerCase());
923
+ const toDesk = agentMeshes.get(msg.recipient.toLowerCase());
356
924
 
357
- if (fromHouse && toHouse && !msg.read) {
925
+ if (fromDesk && toDesk && !msg.read) {
358
926
  createConnectionLine(
359
- fromHouse.position,
360
- toHouse.position
927
+ fromDesk.position,
928
+ toDesk.position
361
929
  );
362
930
  }
363
931
  });
364
932
 
365
- // Update house labels with unread indicators
366
- updateHouseLabels();
933
+ // Update desk labels with unread indicators
934
+ updateDeskLabels();
367
935
  }
368
936
 
369
937
  function animate() {
370
938
  requestAnimationFrame(animate);
939
+
940
+ const time = Date.now() * 0.001;
941
+
942
+ // Rotate holographic sphere
943
+ if (holoSphere) {
944
+ holoSphere.rotation.y = time * 0.15;
945
+ holoSphere.rotation.x = Math.sin(time * 0.1) * 0.1;
946
+ }
947
+
948
+ // Animate floating particles
949
+ floatingParticles.forEach(particles => {
950
+ const positions = particles.geometry.attributes.position.array;
951
+ for (let i = 0; i < positions.length; i += 3) {
952
+ positions[i + 1] += Math.sin(time + positions[i] * 0.1) * 0.003;
953
+ }
954
+ particles.geometry.attributes.position.needsUpdate = true;
955
+ });
956
+
957
+ // Subtle glow pulse on lamps
958
+ glowLights.forEach((item, i) => {
959
+ const pulse = Math.sin(time * 1.5 + i) * 0.15 + 1;
960
+ item.light.intensity = item.baseIntensity * pulse;
961
+ });
962
+
371
963
  controls.update();
372
964
  renderer.render(scene, camera);
373
965
  }
@@ -379,14 +971,12 @@ function onWindowResize() {
379
971
  camera.updateProjectionMatrix();
380
972
  renderer.setSize(width, height);
381
973
 
382
- // Adjust camera position for better mobile view
383
974
  if (width < 768) {
384
- // On mobile, position camera slightly higher and further back
385
- camera.position.y = Math.max(camera.position.y, 35);
386
- camera.position.z = Math.max(camera.position.z, 45);
387
- controls.minDistance = 30; // Prevent zooming too close on mobile
975
+ camera.position.y = Math.max(camera.position.y, 40);
976
+ camera.position.z = Math.max(camera.position.z, 50);
977
+ controls.minDistance = 35;
388
978
  } else {
389
- controls.minDistance = 20;
979
+ controls.minDistance = 25;
390
980
  }
391
981
  }
392
982
 
@@ -410,6 +1000,17 @@ async function loadData() {
410
1000
  recipients = Array.isArray(recipientsData) ? recipientsData : [];
411
1001
  allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
412
1002
 
1003
+ // Load avatar states if avatar DB is configured
1004
+ if (config.avatar) {
1005
+ try {
1006
+ const avatarsRes = await fetch('/api/avatars');
1007
+ avatarStates = await avatarsRes.json();
1008
+ } catch (err) {
1009
+ console.error('Error loading avatar states:', err);
1010
+ avatarStates = {};
1011
+ }
1012
+ }
1013
+
413
1014
  updateUI();
414
1015
  updateVillage();
415
1016
  } catch (err) {
@@ -510,9 +1111,9 @@ function updateUI() {
510
1111
  });
511
1112
  }
512
1113
 
513
- // Update house dialog if it's open
1114
+ // Update desk dialog if it's open
514
1115
  if (document.getElementById('house-dialog').classList.contains('active')) {
515
- updateHouseDialogContent();
1116
+ updateDeskDialogContent();
516
1117
  }
517
1118
  }
518
1119
 
@@ -525,6 +1126,18 @@ async function markAsRead(id) {
525
1126
  }
526
1127
  }
527
1128
 
1129
+ window.markAllAsRead = async function() {
1130
+ const unreadMessages = messages.filter(m => !m.read && m.recipient.toLowerCase() === config.user.toLowerCase());
1131
+ if (unreadMessages.length === 0) return;
1132
+
1133
+ try {
1134
+ await Promise.all(unreadMessages.map(m => fetch(`/api/messages/${m.id}/read`, { method: 'POST' })));
1135
+ loadData();
1136
+ } catch (err) {
1137
+ console.error('Error marking all as read:', err);
1138
+ }
1139
+ };
1140
+
528
1141
  async function sendMessage() {
529
1142
  const to = document.getElementById('recipient-select').value;
530
1143
  const message = document.getElementById('message-input').value.trim();
@@ -561,23 +1174,23 @@ async function sendMessage() {
561
1174
  }
562
1175
  }
563
1176
 
564
- // House click handler
565
- function onHouseClick(event) {
566
- handleHouseInteraction(event.clientX, event.clientY);
1177
+ // Desk click handler
1178
+ function onDeskClick(event) {
1179
+ handleDeskInteraction(event.clientX, event.clientY);
567
1180
  }
568
1181
 
569
1182
  // Touch handlers for mobile
570
1183
  let touchStartX = 0;
571
1184
  let touchStartY = 0;
572
1185
 
573
- function onHouseTouchStart(event) {
1186
+ function onDeskTouchStart(event) {
574
1187
  if (event.touches.length === 1) {
575
1188
  touchStartX = event.touches[0].clientX;
576
1189
  touchStartY = event.touches[0].clientY;
577
1190
  }
578
1191
  }
579
1192
 
580
- function onHouseTouchEnd(event) {
1193
+ function onDeskTouchEnd(event) {
581
1194
  if (event.changedTouches.length === 1) {
582
1195
  const touchEndX = event.changedTouches[0].clientX;
583
1196
  const touchEndY = event.changedTouches[0].clientY;
@@ -590,13 +1203,13 @@ function onHouseTouchEnd(event) {
590
1203
 
591
1204
  // Only trigger if touch didn't move much (tap vs swipe)
592
1205
  if (moveDistance < 20) {
593
- handleHouseInteraction(touchEndX, touchEndY);
1206
+ handleDeskInteraction(touchEndX, touchEndY);
594
1207
  }
595
1208
  }
596
1209
  }
597
1210
 
598
- // Common house interaction handler
599
- function handleHouseInteraction(clientX, clientY) {
1211
+ // Common desk interaction handler
1212
+ function handleDeskInteraction(clientX, clientY) {
600
1213
  // Calculate normalized device coordinates
601
1214
  const rect = renderer.domElement.getBoundingClientRect();
602
1215
  const x = ((clientX - rect.left) / rect.width) * 2 - 1;
@@ -607,34 +1220,34 @@ function handleHouseInteraction(clientX, clientY) {
607
1220
 
608
1221
  raycaster.setFromCamera(mouse, camera);
609
1222
 
610
- // Get all house meshes
611
- const houseMeshes = [];
1223
+ // Get all desk meshes
1224
+ const deskMeshes = [];
612
1225
  agentMeshes.forEach((group, name) => {
613
1226
  group.children.forEach(child => {
614
1227
  if (child.isMesh && !child.userData.isBubble && !child.userData.isCup) {
615
1228
  child.userData.agentName = name;
616
- houseMeshes.push(child);
1229
+ deskMeshes.push(child);
617
1230
  }
618
1231
  });
619
1232
  });
620
1233
 
621
- const intersects = raycaster.intersectObjects(houseMeshes);
1234
+ const intersects = raycaster.intersectObjects(deskMeshes);
622
1235
 
623
1236
  if (intersects.length > 0) {
624
1237
  const agentName = intersects[0].object.userData.agentName;
625
1238
  if (agentName) {
626
- showHouseDialog(agentName);
1239
+ showDeskDialog(agentName);
627
1240
  }
628
1241
  }
629
1242
  }
630
1243
 
631
- // Global variable to track current agent for house dialog
632
- let currentHouseAgent = null;
1244
+ // Global variable to track current agent for desk dialog
1245
+ let currentDeskAgent = null;
633
1246
  let currentTab = 'received';
634
1247
 
635
1248
  // Show dialog with messages for a specific agent
636
- async function showHouseDialog(agentName) {
637
- currentHouseAgent = agentName.toLowerCase();
1249
+ async function showDeskDialog(agentName) {
1250
+ currentDeskAgent = agentName.toLowerCase();
638
1251
  currentTab = 'received'; // Default to received tab
639
1252
 
640
1253
  const dialog = document.getElementById('house-dialog');
@@ -674,35 +1287,35 @@ window.switchTab = function(tab) {
674
1287
  document.getElementById('tab-sent').classList.toggle('active', tab === 'sent');
675
1288
 
676
1289
  // Update content
677
- updateHouseDialogContent();
1290
+ updateDeskDialogContent();
678
1291
  };
679
1292
 
680
- function updateHouseDialogContent() {
1293
+ function updateDeskDialogContent() {
681
1294
  const content = document.getElementById('house-dialog-content');
682
1295
 
683
- if (!currentHouseAgent) return;
1296
+ if (!currentDeskAgent) return;
684
1297
 
685
1298
  // Filter messages based on current tab - FROM THE AGENT'S PERSPECTIVE
686
1299
  let filteredMessages;
687
1300
  if (currentTab === 'received') {
688
1301
  // Messages RECEIVED BY the agent (sent TO the agent)
689
1302
  filteredMessages = allMessages.filter(m =>
690
- m.recipient.toLowerCase() === currentHouseAgent
1303
+ m.recipient.toLowerCase() === currentDeskAgent
691
1304
  );
692
1305
  } else {
693
1306
  // Messages SENT BY the agent
694
1307
  filteredMessages = allMessages.filter(m =>
695
- m.sender.toLowerCase() === currentHouseAgent
1308
+ m.sender.toLowerCase() === currentDeskAgent
696
1309
  );
697
1310
  }
698
1311
 
699
1312
  // Update count badges
700
1313
  const receivedCount = allMessages.filter(m =>
701
- m.recipient.toLowerCase() === currentHouseAgent
1314
+ m.recipient.toLowerCase() === currentDeskAgent
702
1315
  ).length;
703
1316
 
704
1317
  const sentCount = allMessages.filter(m =>
705
- m.sender.toLowerCase() === currentHouseAgent
1318
+ m.sender.toLowerCase() === currentDeskAgent
706
1319
  ).length;
707
1320
 
708
1321
  const receivedBadge = document.getElementById('received-count');
@@ -732,66 +1345,75 @@ function updateHouseDialogContent() {
732
1345
  }
733
1346
  }
734
1347
 
735
- window.closeHouseDialog = function() {
1348
+ window.closeDeskDialog = function() {
736
1349
  document.getElementById('house-dialog').classList.remove('active');
737
1350
  };
738
1351
 
739
- // Update house labels to show unread indicators
740
- function updateHouseLabels() {
1352
+ // Update desk labels to show unread indicators and tool names
1353
+ function updateDeskLabels() {
741
1354
  agentMeshes.forEach((group, name) => {
742
- // Check for unread messages SENT TO this agent (messages they haven't read)
743
1355
  const unreadCount = allMessages.filter(m =>
744
1356
  m.recipient.toLowerCase() === name.toLowerCase() &&
745
1357
  m.sender.toLowerCase() === config.user.toLowerCase() &&
746
1358
  !m.read
747
1359
  ).length;
748
1360
 
749
- // Also check if this agent has sent unread messages TO user
750
1361
  const unreadFromAgent = allMessages.filter(m =>
751
1362
  m.sender.toLowerCase() === name.toLowerCase() &&
752
1363
  m.recipient.toLowerCase() === config.user.toLowerCase() &&
753
1364
  !m.read
754
1365
  ).length;
755
1366
 
756
- // Find the sprite label
1367
+ const avatarState = avatarStates[name.toLowerCase()];
1368
+ const toolName = avatarState?.tool_name || null;
1369
+
757
1370
  const sprite = group.children.find(c => c.isSprite);
758
1371
  if (sprite) {
759
- // Update the canvas texture - high DPI for crisp text
760
1372
  const canvas = document.createElement('canvas');
761
1373
  const context = canvas.getContext('2d');
762
1374
  const scale = 2;
763
1375
  canvas.width = 700;
764
- canvas.height = 128;
1376
+ canvas.height = toolName ? 160 : 128;
765
1377
  context.scale(scale, scale);
766
1378
 
767
- // Background - change color if there are unread messages
1379
+ // Background - themed colors for state
768
1380
  if (unreadFromAgent > 0) {
769
- // Red background for unread messages from agent
770
- context.fillStyle = 'rgba(220, 53, 69, 0.9)';
1381
+ context.fillStyle = 'rgba(220, 80, 80, 0.85)';
771
1382
  } else if (unreadCount > 0) {
772
- // Blue background for messages sent but not read
773
- context.fillStyle = 'rgba(0, 123, 255, 0.9)';
1383
+ context.fillStyle = 'rgba(59, 130, 180, 0.85)';
774
1384
  } else {
775
- // Default black background
776
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
1385
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
777
1386
  }
778
- context.roundRect(0, 0, 350, 64, 16);
1387
+ context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
779
1388
  context.fill();
780
1389
 
781
- // Name
782
- context.font = 'bold 24px Arial';
783
- context.fillStyle = 'white';
1390
+ // Border
1391
+ context.strokeStyle = unreadFromAgent > 0
1392
+ ? 'rgba(255, 120, 120, 0.6)'
1393
+ : unreadCount > 0
1394
+ ? 'rgba(100, 180, 255, 0.6)'
1395
+ : 'rgba(79, 209, 197, 0.3)';
1396
+ context.lineWidth = 1;
1397
+ context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
1398
+ context.stroke();
1399
+
1400
+ context.font = 'bold 22px Arial';
1401
+ context.fillStyle = '#e0f5f0';
784
1402
  context.textAlign = 'center';
785
1403
  context.textBaseline = 'middle';
786
1404
 
787
1405
  if (unreadFromAgent > 0) {
788
- // Show name with unread indicator from agent
789
- context.fillText(`${name} 🔴 ${unreadFromAgent}`, 175, 32);
1406
+ context.fillText(`${name} ${unreadFromAgent}`, 175, 24);
790
1407
  } else if (unreadCount > 0) {
791
- // Show name with sent-but-unread count
792
- context.fillText(`${name} 📤 ${unreadCount}`, 175, 32);
1408
+ context.fillText(`${name} ${unreadCount}`, 175, 24);
793
1409
  } else {
794
- context.fillText(name, 175, 32);
1410
+ context.fillText(name, 175, 24);
1411
+ }
1412
+
1413
+ if (toolName) {
1414
+ context.font = 'italic 14px Arial';
1415
+ context.fillStyle = '#4fd1c5';
1416
+ context.fillText(toolName, 175, 56);
795
1417
  }
796
1418
 
797
1419
  const texture = new THREE.CanvasTexture(canvas);
@@ -799,6 +1421,9 @@ function updateHouseLabels() {
799
1421
  texture.magFilter = THREE.LinearFilter;
800
1422
  sprite.material.map = texture;
801
1423
  sprite.material.needsUpdate = true;
1424
+
1425
+ sprite.position.set(0, 6.5, 2);
1426
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
802
1427
  }
803
1428
  });
804
1429
  }