watercooler 0.0.5 → 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
@@ -4,7 +4,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4
4
  // State
5
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
9
  let avatarStates = {}; // Map of name -> {tool_name, timestamp}
10
10
  let scene, camera, renderer, controls;
@@ -12,10 +12,10 @@ let agentMeshes = new Map();
12
12
  let connectionLines = [];
13
13
  let raycaster, mouse;
14
14
 
15
- // Color palette for agents
15
+ // Color palette for agents - modern muted tones
16
16
  const agentColors = [
17
- 0xFF6B6B, 0x4ECDC4, 0x45B7D1, 0xFFA07A, 0x98D8C8,
18
- 0xF7DC6F, 0xBB8FCE, 0x85C1E2, 0xF8B500, 0x6C5CE7
17
+ 0x5EEAD4, 0x6EE7B7, 0x7DD3FC, 0xA78BFA, 0xFBBF24,
18
+ 0xF9A8D4, 0x86EFAC, 0x93C5FD, 0xC4B5FD, 0x67E8F9
19
19
  ];
20
20
 
21
21
  function getAgentColor(name) {
@@ -26,81 +26,107 @@ function getAgentColor(name) {
26
26
  return agentColors[Math.abs(hash) % agentColors.length];
27
27
  }
28
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
+
29
40
  // Initialize Three.js
30
41
  function init() {
31
42
  const container = document.getElementById('canvas-container');
32
43
 
33
44
  scene = new THREE.Scene();
34
- scene.background = new THREE.Color(0x667eea);
45
+ scene.background = new THREE.Color(0x1a3a3a);
46
+ scene.fog = new THREE.FogExp2(0x1a3a3a, 0.003);
35
47
 
36
- camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
37
- 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);
38
50
 
39
51
  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
40
52
  renderer.setSize(window.innerWidth, window.innerHeight);
53
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
41
54
  renderer.shadowMap.enabled = true;
42
55
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
56
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
57
+ renderer.toneMappingExposure = 1.2;
43
58
  container.appendChild(renderer.domElement);
44
59
 
45
60
  controls = new OrbitControls(camera, renderer.domElement);
46
61
  controls.enableDamping = true;
47
62
  controls.dampingFactor = 0.05;
48
- controls.maxPolarAngle = Math.PI / 2 - 0.1;
49
- controls.minDistance = 20;
50
- controls.maxDistance = 80;
63
+ controls.maxPolarAngle = Math.PI / 2 - 0.05;
64
+ controls.minDistance = 25;
65
+ controls.maxDistance = 120;
51
66
  controls.enableZoom = true;
52
67
  controls.zoomSpeed = 0.8;
53
- controls.enablePan = false; // Disable pan on touch for better mobile UX
68
+ controls.enablePan = false;
69
+ controls.target.set(0, 5, 0);
54
70
  controls.touches = {
55
71
  ONE: THREE.TOUCH.ROTATE,
56
72
  TWO: THREE.TOUCH.DOLLY_PAN
57
73
  };
58
74
 
59
- // Lighting
60
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
75
+ // === Lighting ===
76
+ // Soft ambient
77
+ const ambientLight = new THREE.AmbientLight(0x2d5a5a, 0.8);
61
78
  scene.add(ambientLight);
62
79
 
63
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
64
- 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);
65
83
  dirLight.castShadow = true;
66
- dirLight.shadow.camera.left = -50;
67
- dirLight.shadow.camera.right = 50;
68
- dirLight.shadow.camera.top = 50;
69
- 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;
70
88
  dirLight.shadow.mapSize.width = 2048;
71
89
  dirLight.shadow.mapSize.height = 2048;
90
+ dirLight.shadow.bias = -0.001;
72
91
  scene.add(dirLight);
73
92
 
74
- // Ground
75
- const groundGeo = new THREE.PlaneGeometry(200, 200);
76
- const groundMat = new THREE.MeshStandardMaterial({
77
- color: 0x7dd3c0,
78
- roughness: 0.8
79
- });
80
- const ground = new THREE.Mesh(groundGeo, groundMat);
81
- ground.rotation.x = -Math.PI / 2;
82
- ground.receiveShadow = true;
83
- 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);
84
101
 
85
- // Grid helper
86
- const grid = new THREE.GridHelper(200, 50, 0xffffff, 0xffffff);
87
- grid.material.opacity = 0.2;
88
- grid.material.transparent = true;
89
- scene.add(grid);
102
+ // === Platform ===
103
+ createPlatform();
90
104
 
91
- // Trees
92
- createTrees();
105
+ // === Glass Walls ===
106
+ createGlassWalls();
107
+
108
+ // === Decorative Plants ===
109
+ createPlants();
110
+
111
+ // === Holographic Sphere ===
112
+ createHolographicSphere();
113
+
114
+ // === Ambient Glow Lights ===
115
+ createGlowLights();
116
+
117
+ // === Floating Particles ===
118
+ createFloatingParticles();
93
119
 
94
120
  window.addEventListener('resize', onWindowResize);
95
121
 
96
- // Raycaster for house clicks
122
+ // Raycaster for desk clicks
97
123
  raycaster = new THREE.Raycaster();
98
124
  mouse = new THREE.Vector2();
99
- renderer.domElement.addEventListener('click', onHouseClick);
125
+ renderer.domElement.addEventListener('click', onDeskClick);
100
126
 
101
127
  // Add touch support for mobile
102
- renderer.domElement.addEventListener('touchstart', onHouseTouchStart, { passive: false });
103
- renderer.domElement.addEventListener('touchend', onHouseTouchEnd, { passive: false });
128
+ renderer.domElement.addEventListener('touchstart', onDeskTouchStart, { passive: false });
129
+ renderer.domElement.addEventListener('touchend', onDeskTouchEnd, { passive: false });
104
130
 
105
131
  // Disable context menu on mobile for better UX
106
132
  renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
@@ -113,102 +139,551 @@ function init() {
113
139
  animate();
114
140
  }
115
141
 
116
- function createTrees() {
117
- for (let i = 0; i < 30; i++) {
118
- const x = (Math.random() - 0.5) * 150;
119
- const z = (Math.random() - 0.5) * 150;
120
-
121
- // Don't place trees too close to center
122
- if (Math.sqrt(x*x + z*z) < 30) continue;
123
-
124
- const trunkGeo = new THREE.CylinderGeometry(0.5, 0.8, 3, 8);
125
- const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
126
- const trunk = new THREE.Mesh(trunkGeo, trunkMat);
127
- trunk.position.set(x, 1.5, z);
128
- 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);
129
447
 
130
- const leavesGeo = new THREE.ConeGeometry(3, 8, 8);
131
- const leavesMat = new THREE.MeshStandardMaterial({ color: 0x228B22 });
132
- const leaves = new THREE.Mesh(leavesGeo, leavesMat);
133
- leaves.position.set(x, 6, z);
134
- 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);
135
458
 
136
- scene.add(trunk);
137
- 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;
138
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);
139
492
  }
140
493
 
141
- function createAgentHouse(name, position, toolName = null) {
494
+ function createAgentDesk(name, position, toolName = null) {
142
495
  const color = getAgentColor(name);
143
496
  const group = new THREE.Group();
144
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
+ });
145
531
 
146
- // House base
147
- const baseGeo = new THREE.BoxGeometry(6, 4, 6);
148
- const baseMat = new THREE.MeshStandardMaterial({ color: color });
149
- const base = new THREE.Mesh(baseGeo, baseMat);
150
- base.position.y = 2;
151
- base.castShadow = true;
152
- base.receiveShadow = true;
153
- group.add(base);
154
-
155
- // Roof
156
- const roofGeo = new THREE.ConeGeometry(5, 3, 4);
157
- const roofMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
158
- const roof = new THREE.Mesh(roofGeo, roofMat);
159
- roof.position.y = 5.5;
160
- roof.rotation.y = Math.PI / 4;
161
- roof.castShadow = true;
162
- group.add(roof);
163
-
164
- // Door
165
- const doorGeo = new THREE.BoxGeometry(1.5, 2.5, 0.2);
166
- const doorMat = new THREE.MeshStandardMaterial({ color: 0x4a3c28 });
167
- const door = new THREE.Mesh(doorGeo, doorMat);
168
- door.position.set(0, 1.25, 3.1);
169
- group.add(door);
170
-
171
- // Windows
172
- const windowGeo = new THREE.BoxGeometry(1.2, 1.2, 0.2);
173
- const windowMat = new THREE.MeshStandardMaterial({
174
- color: 0xFFFF99,
175
- emissive: 0xFFFF99,
176
- emissiveIntensity: 0.3
177
- });
178
-
179
- const window1 = new THREE.Mesh(windowGeo, windowMat);
180
- window1.position.set(-1.8, 2.5, 3.1);
181
- group.add(window1);
182
-
183
- const window2 = new THREE.Mesh(windowGeo, windowMat);
184
- window2.position.set(1.8, 2.5, 3.1);
185
- group.add(window2);
186
-
187
- // Name label sprite (with optional tool name)
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
+ }
571
+
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);
658
+
659
+ // Name label sprite
188
660
  const canvas = document.createElement('canvas');
189
661
  const context = canvas.getContext('2d');
190
- // High DPI canvas for crisp text
191
662
  const scale = 2;
192
663
  canvas.width = 512;
193
- canvas.height = toolName ? 160 : 128; // Taller if showing tool
664
+ canvas.height = toolName ? 160 : 128;
194
665
  context.scale(scale, scale);
195
666
 
196
- // Background
197
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
667
+ // Frosted glass background
668
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
198
669
  context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
199
670
  context.fill();
200
671
 
201
- // Name text
202
- context.font = 'bold 24px Arial';
203
- context.fillStyle = 'white';
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';
204
680
  context.textAlign = 'center';
205
681
  context.textBaseline = 'middle';
206
682
  context.fillText(name, 128, 24);
207
683
 
208
- // Tool name text (if available)
209
684
  if (toolName) {
210
- context.font = 'italic 16px Arial';
211
- context.fillStyle = '#FFD700'; // Gold color for tool name
685
+ context.font = 'italic 14px Arial';
686
+ context.fillStyle = '#4fd1c5';
212
687
  context.fillText(toolName, 128, 56);
213
688
  }
214
689
 
@@ -217,30 +692,21 @@ function createAgentHouse(name, position, toolName = null) {
217
692
  texture.magFilter = THREE.LinearFilter;
218
693
  const spriteMat = new THREE.SpriteMaterial({ map: texture });
219
694
  const sprite = new THREE.Sprite(spriteMat);
220
- sprite.position.set(0, toolName ? 8.5 : 8, 0);
221
- sprite.scale.set(8, toolName ? 2.5 : 2, 1);
222
- sprite.name = 'label'; // Tag for easy updates
695
+ sprite.position.set(0, 6.5, 2);
696
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
697
+ sprite.name = 'label';
223
698
  group.add(sprite);
224
699
 
225
- // Path to house
226
- const pathGeo = new THREE.PlaneGeometry(2, 8);
227
- const pathMat = new THREE.MeshStandardMaterial({ color: 0xD2B48C });
228
- const path = new THREE.Mesh(pathGeo, pathMat);
229
- path.rotation.x = -Math.PI / 2;
230
- path.position.set(0, 0.02, 7);
231
- group.add(path);
232
-
233
700
  scene.add(group);
234
701
  agentMeshes.set(name, group);
235
702
 
236
703
  return group;
237
704
  }
238
705
 
239
- function updateHouseLabel(house, name, toolName = null) {
240
- const sprite = house.getObjectByName('label');
706
+ function updateDeskLabel(desk, name, toolName = null) {
707
+ const sprite = desk.getObjectByName('label');
241
708
  if (!sprite) return;
242
709
 
243
- // Create new canvas with updated text
244
710
  const canvas = document.createElement('canvas');
245
711
  const context = canvas.getContext('2d');
246
712
  const scale = 2;
@@ -248,67 +714,73 @@ function updateHouseLabel(house, name, toolName = null) {
248
714
  canvas.height = toolName ? 160 : 128;
249
715
  context.scale(scale, scale);
250
716
 
251
- // Background
252
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
717
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
253
718
  context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
254
719
  context.fill();
255
720
 
256
- // Name text
257
- context.font = 'bold 24px Arial';
258
- context.fillStyle = 'white';
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';
259
728
  context.textAlign = 'center';
260
729
  context.textBaseline = 'middle';
261
730
  context.fillText(name, 128, 24);
262
731
 
263
- // Tool name text (if available)
264
732
  if (toolName) {
265
- context.font = 'italic 16px Arial';
266
- context.fillStyle = '#FFD700';
733
+ context.font = 'italic 14px Arial';
734
+ context.fillStyle = '#4fd1c5';
267
735
  context.fillText(toolName, 128, 56);
268
736
  }
269
737
 
270
- // Update texture
271
738
  const texture = new THREE.CanvasTexture(canvas);
272
739
  texture.minFilter = THREE.LinearFilter;
273
740
  texture.magFilter = THREE.LinearFilter;
274
741
  sprite.material.map = texture;
275
742
  sprite.material.needsUpdate = true;
276
743
 
277
- // Update position and scale
278
- sprite.position.set(0, toolName ? 8.5 : 8, 0);
279
- sprite.scale.set(8, toolName ? 2.5 : 2, 1);
744
+ sprite.position.set(0, 6.5, 2);
745
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
280
746
  }
281
747
 
282
748
  function createMessageParticle(fromPos, toPos) {
283
- const particleGeo = new THREE.SphereGeometry(0.3, 8, 8);
284
- const particleMat = new THREE.MeshStandardMaterial({
285
- color: 0xFFD700,
286
- emissive: 0xFFD700,
287
- 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
288
754
  });
289
755
  const particle = new THREE.Mesh(particleGeo, particleMat);
290
756
 
291
757
  particle.position.copy(fromPos);
292
- 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);
293
763
 
294
764
  scene.add(particle);
295
765
 
296
- // Animate particle
297
766
  const startTime = Date.now();
298
- const duration = 2000;
767
+ const duration = 1500;
299
768
 
300
769
  function animateParticle() {
301
770
  const elapsed = Date.now() - startTime;
302
771
  const progress = Math.min(elapsed / duration, 1);
303
772
 
304
773
  particle.position.lerpVectors(
305
- new THREE.Vector3(fromPos.x, fromPos.y + 8, fromPos.z),
306
- 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),
307
776
  progress
308
777
  );
309
778
 
310
- // Add arc
311
- 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);
312
784
 
313
785
  if (progress < 1) {
314
786
  requestAnimationFrame(animateParticle);
@@ -324,48 +796,58 @@ function createConnectionLine(fromPos, toPos) {
324
796
  const startPos = new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z);
325
797
  const endPos = new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z);
326
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
327
807
  const material = new THREE.LineBasicMaterial({
328
808
  color: 0xff6b6b,
329
- opacity: 0.8,
809
+ opacity: 0.7,
330
810
  transparent: true,
331
811
  linewidth: 3
332
812
  });
333
813
 
334
- const points = [startPos, endPos];
335
-
336
814
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
337
815
  const line = new THREE.Line(geometry, material);
338
816
 
339
817
  scene.add(line);
340
818
  connectionLines.push(line);
341
819
 
342
- // Add arrowhead for directionality
820
+ // Add small chevron markers along the path to show direction
343
821
  const direction = new THREE.Vector3().subVectors(endPos, startPos).normalize();
344
- const arrowPos = endPos.clone().sub(direction.clone().multiplyScalar(5));
345
-
346
- const arrowGeometry = new THREE.ConeGeometry(0.5, 1.5, 8);
347
- const arrowMaterial = new THREE.MeshStandardMaterial({
348
- color: 0xff6b6b,
349
- emissive: 0xff6b6b,
350
- emissiveIntensity: 0.3
351
- });
352
- const arrowhead = new THREE.Mesh(arrowGeometry, arrowMaterial);
353
-
354
- arrowhead.position.copy(arrowPos);
355
-
356
- // Orient arrow to point along the line
357
822
  const up = new THREE.Vector3(0, 1, 0);
358
- const quaternion = new THREE.Quaternion();
359
- quaternion.setFromUnitVectors(up, direction);
360
- arrowhead.setRotationFromQuaternion(quaternion);
361
-
362
- scene.add(arrowhead);
363
- 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
+ }
364
847
 
365
- // Send particle
366
848
  setTimeout(() => {
367
849
  createMessageParticle(fromPos, toPos);
368
- }, 100);
850
+ }, 50);
369
851
  }
370
852
 
371
853
  function clearConnections() {
@@ -377,49 +859,47 @@ function updateVillage() {
377
859
  clearConnections();
378
860
 
379
861
  // Use recipients (from coworkers.db) as the authoritative list of agents
380
- // This ensures we only show houses for registered coworkers
381
862
  const allAgents = new Set([config.user.toLowerCase(), ...recipients.map(r => r.toLowerCase())]);
382
863
 
383
- // Also add message participants that might not be in coworker db yet
864
+ // Also add message participants
384
865
  messages.forEach(m => {
385
866
  allAgents.add(m.sender.toLowerCase());
386
867
  allAgents.add(m.recipient.toLowerCase());
387
868
  });
388
869
 
389
- // Arrange agents in a circle
870
+ // Arrange agents in a circle on the platform
390
871
  const agents = Array.from(allAgents);
391
- const radius = 25;
872
+ const radius = Math.min(20, Math.max(10, agents.length * 3));
392
873
 
393
874
  agents.forEach((agent, index) => {
394
- const angle = (index / agents.length) * Math.PI * 2;
875
+ const angle = (index / agents.length) * Math.PI * 2 - Math.PI / 2;
395
876
  const x = Math.cos(angle) * radius;
396
877
  const z = Math.sin(angle) * radius;
397
878
  const position = new THREE.Vector3(x, 0, z);
398
879
 
399
- // Get tool name for this agent from avatar states
400
880
  const avatarState = avatarStates[agent.toLowerCase()];
401
881
  const toolName = avatarState?.tool_name || null;
402
882
 
403
883
  if (!agentMeshes.has(agent)) {
404
- createAgentHouse(agent, position, toolName);
884
+ const group = createAgentDesk(agent, position, toolName);
885
+ // Face desk toward center
886
+ group.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
405
887
  } else {
406
- // Update position if needed
407
- const house = agentMeshes.get(agent);
408
- house.position.copy(position);
409
-
410
- // Update label with current tool name
411
- updateHouseLabel(house, agent, toolName);
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);
412
892
  }
413
893
  });
414
894
 
415
- // Remove houses for coworkers that no longer exist
416
- agentMeshes.forEach((house, name) => {
895
+ // Remove desks for coworkers that no longer exist
896
+ agentMeshes.forEach((desk, name) => {
417
897
  if (!allAgents.has(name)) {
418
898
  // Remove from scene
419
- scene.remove(house);
899
+ scene.remove(desk);
420
900
 
421
901
  // Dispose of geometries and materials to prevent memory leaks
422
- house.traverse((child) => {
902
+ desk.traverse((child) => {
423
903
  if (child.isMesh) {
424
904
  child.geometry.dispose();
425
905
  if (child.material) {
@@ -439,23 +919,47 @@ function updateVillage() {
439
919
 
440
920
  // Create connections for unread messages only
441
921
  allMessages.forEach(msg => {
442
- const fromHouse = agentMeshes.get(msg.sender.toLowerCase());
443
- const toHouse = agentMeshes.get(msg.recipient.toLowerCase());
922
+ const fromDesk = agentMeshes.get(msg.sender.toLowerCase());
923
+ const toDesk = agentMeshes.get(msg.recipient.toLowerCase());
444
924
 
445
- if (fromHouse && toHouse && !msg.read) {
925
+ if (fromDesk && toDesk && !msg.read) {
446
926
  createConnectionLine(
447
- fromHouse.position,
448
- toHouse.position
927
+ fromDesk.position,
928
+ toDesk.position
449
929
  );
450
930
  }
451
931
  });
452
932
 
453
- // Update house labels with unread indicators
454
- updateHouseLabels();
933
+ // Update desk labels with unread indicators
934
+ updateDeskLabels();
455
935
  }
456
936
 
457
937
  function animate() {
458
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
+
459
963
  controls.update();
460
964
  renderer.render(scene, camera);
461
965
  }
@@ -467,14 +971,12 @@ function onWindowResize() {
467
971
  camera.updateProjectionMatrix();
468
972
  renderer.setSize(width, height);
469
973
 
470
- // Adjust camera position for better mobile view
471
974
  if (width < 768) {
472
- // On mobile, position camera slightly higher and further back
473
- camera.position.y = Math.max(camera.position.y, 35);
474
- camera.position.z = Math.max(camera.position.z, 45);
475
- 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;
476
978
  } else {
477
- controls.minDistance = 20;
979
+ controls.minDistance = 25;
478
980
  }
479
981
  }
480
982
 
@@ -609,9 +1111,9 @@ function updateUI() {
609
1111
  });
610
1112
  }
611
1113
 
612
- // Update house dialog if it's open
1114
+ // Update desk dialog if it's open
613
1115
  if (document.getElementById('house-dialog').classList.contains('active')) {
614
- updateHouseDialogContent();
1116
+ updateDeskDialogContent();
615
1117
  }
616
1118
  }
617
1119
 
@@ -672,23 +1174,23 @@ async function sendMessage() {
672
1174
  }
673
1175
  }
674
1176
 
675
- // House click handler
676
- function onHouseClick(event) {
677
- handleHouseInteraction(event.clientX, event.clientY);
1177
+ // Desk click handler
1178
+ function onDeskClick(event) {
1179
+ handleDeskInteraction(event.clientX, event.clientY);
678
1180
  }
679
1181
 
680
1182
  // Touch handlers for mobile
681
1183
  let touchStartX = 0;
682
1184
  let touchStartY = 0;
683
1185
 
684
- function onHouseTouchStart(event) {
1186
+ function onDeskTouchStart(event) {
685
1187
  if (event.touches.length === 1) {
686
1188
  touchStartX = event.touches[0].clientX;
687
1189
  touchStartY = event.touches[0].clientY;
688
1190
  }
689
1191
  }
690
1192
 
691
- function onHouseTouchEnd(event) {
1193
+ function onDeskTouchEnd(event) {
692
1194
  if (event.changedTouches.length === 1) {
693
1195
  const touchEndX = event.changedTouches[0].clientX;
694
1196
  const touchEndY = event.changedTouches[0].clientY;
@@ -701,13 +1203,13 @@ function onHouseTouchEnd(event) {
701
1203
 
702
1204
  // Only trigger if touch didn't move much (tap vs swipe)
703
1205
  if (moveDistance < 20) {
704
- handleHouseInteraction(touchEndX, touchEndY);
1206
+ handleDeskInteraction(touchEndX, touchEndY);
705
1207
  }
706
1208
  }
707
1209
  }
708
1210
 
709
- // Common house interaction handler
710
- function handleHouseInteraction(clientX, clientY) {
1211
+ // Common desk interaction handler
1212
+ function handleDeskInteraction(clientX, clientY) {
711
1213
  // Calculate normalized device coordinates
712
1214
  const rect = renderer.domElement.getBoundingClientRect();
713
1215
  const x = ((clientX - rect.left) / rect.width) * 2 - 1;
@@ -718,34 +1220,34 @@ function handleHouseInteraction(clientX, clientY) {
718
1220
 
719
1221
  raycaster.setFromCamera(mouse, camera);
720
1222
 
721
- // Get all house meshes
722
- const houseMeshes = [];
1223
+ // Get all desk meshes
1224
+ const deskMeshes = [];
723
1225
  agentMeshes.forEach((group, name) => {
724
1226
  group.children.forEach(child => {
725
1227
  if (child.isMesh && !child.userData.isBubble && !child.userData.isCup) {
726
1228
  child.userData.agentName = name;
727
- houseMeshes.push(child);
1229
+ deskMeshes.push(child);
728
1230
  }
729
1231
  });
730
1232
  });
731
1233
 
732
- const intersects = raycaster.intersectObjects(houseMeshes);
1234
+ const intersects = raycaster.intersectObjects(deskMeshes);
733
1235
 
734
1236
  if (intersects.length > 0) {
735
1237
  const agentName = intersects[0].object.userData.agentName;
736
1238
  if (agentName) {
737
- showHouseDialog(agentName);
1239
+ showDeskDialog(agentName);
738
1240
  }
739
1241
  }
740
1242
  }
741
1243
 
742
- // Global variable to track current agent for house dialog
743
- let currentHouseAgent = null;
1244
+ // Global variable to track current agent for desk dialog
1245
+ let currentDeskAgent = null;
744
1246
  let currentTab = 'received';
745
1247
 
746
1248
  // Show dialog with messages for a specific agent
747
- async function showHouseDialog(agentName) {
748
- currentHouseAgent = agentName.toLowerCase();
1249
+ async function showDeskDialog(agentName) {
1250
+ currentDeskAgent = agentName.toLowerCase();
749
1251
  currentTab = 'received'; // Default to received tab
750
1252
 
751
1253
  const dialog = document.getElementById('house-dialog');
@@ -785,35 +1287,35 @@ window.switchTab = function(tab) {
785
1287
  document.getElementById('tab-sent').classList.toggle('active', tab === 'sent');
786
1288
 
787
1289
  // Update content
788
- updateHouseDialogContent();
1290
+ updateDeskDialogContent();
789
1291
  };
790
1292
 
791
- function updateHouseDialogContent() {
1293
+ function updateDeskDialogContent() {
792
1294
  const content = document.getElementById('house-dialog-content');
793
1295
 
794
- if (!currentHouseAgent) return;
1296
+ if (!currentDeskAgent) return;
795
1297
 
796
1298
  // Filter messages based on current tab - FROM THE AGENT'S PERSPECTIVE
797
1299
  let filteredMessages;
798
1300
  if (currentTab === 'received') {
799
1301
  // Messages RECEIVED BY the agent (sent TO the agent)
800
1302
  filteredMessages = allMessages.filter(m =>
801
- m.recipient.toLowerCase() === currentHouseAgent
1303
+ m.recipient.toLowerCase() === currentDeskAgent
802
1304
  );
803
1305
  } else {
804
1306
  // Messages SENT BY the agent
805
1307
  filteredMessages = allMessages.filter(m =>
806
- m.sender.toLowerCase() === currentHouseAgent
1308
+ m.sender.toLowerCase() === currentDeskAgent
807
1309
  );
808
1310
  }
809
1311
 
810
1312
  // Update count badges
811
1313
  const receivedCount = allMessages.filter(m =>
812
- m.recipient.toLowerCase() === currentHouseAgent
1314
+ m.recipient.toLowerCase() === currentDeskAgent
813
1315
  ).length;
814
1316
 
815
1317
  const sentCount = allMessages.filter(m =>
816
- m.sender.toLowerCase() === currentHouseAgent
1318
+ m.sender.toLowerCase() === currentDeskAgent
817
1319
  ).length;
818
1320
 
819
1321
  const receivedBadge = document.getElementById('received-count');
@@ -843,35 +1345,30 @@ function updateHouseDialogContent() {
843
1345
  }
844
1346
  }
845
1347
 
846
- window.closeHouseDialog = function() {
1348
+ window.closeDeskDialog = function() {
847
1349
  document.getElementById('house-dialog').classList.remove('active');
848
1350
  };
849
1351
 
850
- // Update house labels to show unread indicators and tool names
851
- function updateHouseLabels() {
1352
+ // Update desk labels to show unread indicators and tool names
1353
+ function updateDeskLabels() {
852
1354
  agentMeshes.forEach((group, name) => {
853
- // Check for unread messages SENT TO this agent (messages they haven't read)
854
1355
  const unreadCount = allMessages.filter(m =>
855
1356
  m.recipient.toLowerCase() === name.toLowerCase() &&
856
1357
  m.sender.toLowerCase() === config.user.toLowerCase() &&
857
1358
  !m.read
858
1359
  ).length;
859
1360
 
860
- // Also check if this agent has sent unread messages TO user
861
1361
  const unreadFromAgent = allMessages.filter(m =>
862
1362
  m.sender.toLowerCase() === name.toLowerCase() &&
863
1363
  m.recipient.toLowerCase() === config.user.toLowerCase() &&
864
1364
  !m.read
865
1365
  ).length;
866
1366
 
867
- // Get tool name for this agent
868
1367
  const avatarState = avatarStates[name.toLowerCase()];
869
1368
  const toolName = avatarState?.tool_name || null;
870
1369
 
871
- // Find the sprite label
872
1370
  const sprite = group.children.find(c => c.isSprite);
873
1371
  if (sprite) {
874
- // Update the canvas texture - high DPI for crisp text
875
1372
  const canvas = document.createElement('canvas');
876
1373
  const context = canvas.getContext('2d');
877
1374
  const scale = 2;
@@ -879,40 +1376,43 @@ function updateHouseLabels() {
879
1376
  canvas.height = toolName ? 160 : 128;
880
1377
  context.scale(scale, scale);
881
1378
 
882
- // Background - change color if there are unread messages
1379
+ // Background - themed colors for state
883
1380
  if (unreadFromAgent > 0) {
884
- // Red background for unread messages from agent
885
- context.fillStyle = 'rgba(220, 53, 69, 0.9)';
1381
+ context.fillStyle = 'rgba(220, 80, 80, 0.85)';
886
1382
  } else if (unreadCount > 0) {
887
- // Blue background for messages sent but not read
888
- context.fillStyle = 'rgba(0, 123, 255, 0.9)';
1383
+ context.fillStyle = 'rgba(59, 130, 180, 0.85)';
889
1384
  } else {
890
- // Default black background
891
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
1385
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
892
1386
  }
893
1387
  context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
894
1388
  context.fill();
895
1389
 
896
- // Name
897
- context.font = 'bold 24px Arial';
898
- 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';
899
1402
  context.textAlign = 'center';
900
1403
  context.textBaseline = 'middle';
901
1404
 
902
1405
  if (unreadFromAgent > 0) {
903
- // Show name with unread indicator from agent
904
- context.fillText(`${name} 🔴 ${unreadFromAgent}`, 175, 24);
1406
+ context.fillText(`${name} ${unreadFromAgent}`, 175, 24);
905
1407
  } else if (unreadCount > 0) {
906
- // Show name with sent-but-unread count
907
- context.fillText(`${name} 📤 ${unreadCount}`, 175, 24);
1408
+ context.fillText(`${name} ${unreadCount}`, 175, 24);
908
1409
  } else {
909
1410
  context.fillText(name, 175, 24);
910
1411
  }
911
1412
 
912
- // Tool name text (if available)
913
1413
  if (toolName) {
914
- context.font = 'italic 16px Arial';
915
- context.fillStyle = '#FFD700'; // Gold color for tool name
1414
+ context.font = 'italic 14px Arial';
1415
+ context.fillStyle = '#4fd1c5';
916
1416
  context.fillText(toolName, 175, 56);
917
1417
  }
918
1418
 
@@ -922,9 +1422,8 @@ function updateHouseLabels() {
922
1422
  sprite.material.map = texture;
923
1423
  sprite.material.needsUpdate = true;
924
1424
 
925
- // Update position and scale based on whether tool name is shown
926
- sprite.position.set(0, toolName ? 8.5 : 8, 0);
927
- sprite.scale.set(8, toolName ? 2.5 : 2, 1);
1425
+ sprite.position.set(0, 6.5, 2);
1426
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
928
1427
  }
929
1428
  });
930
1429
  }