watercooler 0.0.6 → 0.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watercooler",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "A beautiful 3D visualization of your mailbox messages as a village of coworkers",
5
5
  "type": "module",
6
6
  "main": "server.ts",
package/public/app.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import * as THREE from 'three';
2
2
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
4
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
5
+ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
3
6
 
4
7
  // State
5
8
  let config = { user: '', mailbox: '', avatar: null };
@@ -36,6 +39,8 @@ let holoSphere = null;
36
39
  let holoParticles = null;
37
40
  let glowLights = [];
38
41
  let floatingParticles = [];
42
+ let composer = null;
43
+ let waterMesh = null;
39
44
 
40
45
  // Initialize Three.js
41
46
  function init() {
@@ -99,9 +104,27 @@ function init() {
99
104
  const hemiLight = new THREE.HemisphereLight(0x4fd1c5, 0x1a3a3a, 0.4);
100
105
  scene.add(hemiLight);
101
106
 
107
+ // Additional accent lights for bloom effect
108
+ // Center glow from holographic sphere area
109
+ const centerGlow = new THREE.PointLight(0x4fd1c5, 0.6, 50);
110
+ centerGlow.position.set(0, PLATFORM_HEIGHT + 15, 0);
111
+ scene.add(centerGlow);
112
+
113
+ // Edge accent lights
114
+ const edgeLight1 = new THREE.PointLight(0x88ffdd, 0.4, 30);
115
+ edgeLight1.position.set(30, PLATFORM_HEIGHT + 10, 30);
116
+ scene.add(edgeLight1);
117
+
118
+ const edgeLight2 = new THREE.PointLight(0x88ffdd, 0.4, 30);
119
+ edgeLight2.position.set(-30, PLATFORM_HEIGHT + 10, -30);
120
+ scene.add(edgeLight2);
121
+
102
122
  // === Platform ===
103
123
  createPlatform();
104
124
 
125
+ // === Reflective Water Surface ===
126
+ createReflectiveWater();
127
+
105
128
  // === Glass Walls ===
106
129
  createGlassWalls();
107
130
 
@@ -117,6 +140,9 @@ function init() {
117
140
  // === Floating Particles ===
118
141
  createFloatingParticles();
119
142
 
143
+ // === Background Stars ===
144
+ createBackgroundStars();
145
+
120
146
  window.addEventListener('resize', onWindowResize);
121
147
 
122
148
  // Raycaster for desk clicks
@@ -136,6 +162,9 @@ function init() {
136
162
  setTimeout(onWindowResize, 100);
137
163
  });
138
164
 
165
+ // === Post Processing ===
166
+ setupPostProcessing();
167
+
139
168
  animate();
140
169
  }
141
170
 
@@ -198,6 +227,56 @@ function createPlatform() {
198
227
  scene.add(ground);
199
228
  }
200
229
 
230
+ function createReflectiveWater() {
231
+ // Reflective water surface below the platform
232
+ const waterSize = PLATFORM_SIZE * 1.5;
233
+ const waterGeo = new THREE.PlaneGeometry(waterSize, waterSize, 64, 64);
234
+
235
+ // Create a custom shader material for reflective water effect
236
+ const waterMat = new THREE.MeshPhysicalMaterial({
237
+ color: 0x0d3333,
238
+ metalness: 0.9,
239
+ roughness: 0.1,
240
+ transparent: true,
241
+ opacity: 0.85,
242
+ transmission: 0.3,
243
+ thickness: 0.5,
244
+ clearcoat: 1.0,
245
+ clearcoatRoughness: 0.1,
246
+ side: THREE.DoubleSide
247
+ });
248
+
249
+ waterMesh = new THREE.Mesh(waterGeo, waterMat);
250
+ waterMesh.rotation.x = -Math.PI / 2;
251
+ waterMesh.position.y = -0.5;
252
+ waterMesh.receiveShadow = true;
253
+ scene.add(waterMesh);
254
+
255
+ // Add subtle ripple effect using vertex displacement
256
+ const positions = waterMesh.geometry.attributes.position;
257
+ const initialPositions = positions.array.slice();
258
+ waterMesh.userData.initialPositions = initialPositions;
259
+ waterMesh.userData.ripplePhase = 0;
260
+ }
261
+
262
+ function setupPostProcessing() {
263
+ // Setup EffectComposer for bloom
264
+ composer = new EffectComposer(renderer);
265
+
266
+ // Add render pass
267
+ const renderPass = new RenderPass(scene, camera);
268
+ composer.addPass(renderPass);
269
+
270
+ // Add bloom pass
271
+ const bloomPass = new UnrealBloomPass(
272
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
273
+ 0.8, // strength
274
+ 0.4, // radius
275
+ 0.75 // threshold
276
+ );
277
+ composer.addPass(bloomPass);
278
+ }
279
+
201
280
  function createGlassWalls() {
202
281
  const glassMat = new THREE.MeshPhysicalMaterial({
203
282
  color: 0x88cccc,
@@ -491,6 +570,48 @@ function createFloatingParticles() {
491
570
  floatingParticles.push(particles);
492
571
  }
493
572
 
573
+ function createBackgroundStars() {
574
+ // Distant stars/sparkles in the background
575
+ const starCount = 200;
576
+ const positions = new Float32Array(starCount * 3);
577
+ const sizes = new Float32Array(starCount);
578
+
579
+ for (let i = 0; i < starCount; i++) {
580
+ // Place stars far outside the platform
581
+ const theta = Math.random() * Math.PI * 2;
582
+ const phi = Math.acos(2 * Math.random() - 1);
583
+ const radius = 100 + Math.random() * 150;
584
+
585
+ positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
586
+ positions[i * 3 + 1] = 20 + Math.random() * 100;
587
+ positions[i * 3 + 2] = radius * Math.sin(phi) * Math.sin(theta);
588
+
589
+ sizes[i] = 0.5 + Math.random() * 1.5;
590
+ }
591
+
592
+ const geometry = new THREE.BufferGeometry();
593
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
594
+ geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
595
+
596
+ const material = new THREE.PointsMaterial({
597
+ color: 0xaaddff,
598
+ size: 1.0,
599
+ transparent: true,
600
+ opacity: 0.6,
601
+ blending: THREE.AdditiveBlending,
602
+ sizeAttenuation: true
603
+ });
604
+
605
+ const stars = new THREE.Points(geometry, material);
606
+ scene.add(stars);
607
+
608
+ // Animate stars with twinkle effect
609
+ stars.userData.twinklePhase = Math.random() * Math.PI * 2;
610
+
611
+ // Add to floatingParticles for animation
612
+ floatingParticles.push(stars);
613
+ }
614
+
494
615
  function createAgentDesk(name, position, toolName = null) {
495
616
  const color = getAgentColor(name);
496
617
  const group = new THREE.Group();
@@ -946,12 +1067,23 @@ function animate() {
946
1067
  }
947
1068
 
948
1069
  // Animate floating particles
949
- floatingParticles.forEach(particles => {
1070
+ floatingParticles.forEach((particles, index) => {
950
1071
  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;
1072
+
1073
+ if (particles.userData.twinklePhase !== undefined) {
1074
+ // Star twinkling effect
1075
+ const twinkle = Math.sin(time * 2 + particles.userData.twinklePhase) * 0.3 + 0.7;
1076
+ particles.material.opacity = 0.4 + twinkle * 0.4;
1077
+
1078
+ // Slowly rotate stars
1079
+ particles.rotation.y = time * 0.02;
1080
+ } else {
1081
+ // Regular floating particles
1082
+ for (let i = 0; i < positions.length; i += 3) {
1083
+ positions[i + 1] += Math.sin(time + positions[i] * 0.1) * 0.003;
1084
+ }
1085
+ particles.geometry.attributes.position.needsUpdate = true;
953
1086
  }
954
- particles.geometry.attributes.position.needsUpdate = true;
955
1087
  });
956
1088
 
957
1089
  // Subtle glow pulse on lamps
@@ -960,8 +1092,34 @@ function animate() {
960
1092
  item.light.intensity = item.baseIntensity * pulse;
961
1093
  });
962
1094
 
1095
+ // Animate water ripples
1096
+ if (waterMesh && waterMesh.userData.initialPositions) {
1097
+ const positions = waterMesh.geometry.attributes.position;
1098
+ const initialPositions = waterMesh.userData.initialPositions;
1099
+
1100
+ for (let i = 0; i < positions.count; i++) {
1101
+ const x = initialPositions[i * 3];
1102
+ const y = initialPositions[i * 3 + 1];
1103
+
1104
+ // Create gentle ripple effect
1105
+ const distance = Math.sqrt(x * x + y * y);
1106
+ const wave1 = Math.sin(distance * 0.3 - time * 0.8) * 0.15;
1107
+ const wave2 = Math.sin(x * 0.2 + time * 0.5) * 0.1;
1108
+ const wave3 = Math.cos(y * 0.15 + time * 0.3) * 0.08;
1109
+
1110
+ positions.setZ(i, wave1 + wave2 + wave3);
1111
+ }
1112
+ positions.needsUpdate = true;
1113
+ }
1114
+
963
1115
  controls.update();
964
- renderer.render(scene, camera);
1116
+
1117
+ // Use composer for bloom effect if available, otherwise standard renderer
1118
+ if (composer) {
1119
+ composer.render();
1120
+ } else {
1121
+ renderer.render(scene, camera);
1122
+ }
965
1123
  }
966
1124
 
967
1125
  function onWindowResize() {
@@ -971,6 +1129,11 @@ function onWindowResize() {
971
1129
  camera.updateProjectionMatrix();
972
1130
  renderer.setSize(width, height);
973
1131
 
1132
+ // Resize composer for bloom effect
1133
+ if (composer) {
1134
+ composer.setSize(width, height);
1135
+ }
1136
+
974
1137
  if (width < 768) {
975
1138
  camera.position.y = Math.max(camera.position.y, 40);
976
1139
  camera.position.z = Math.max(camera.position.z, 50);
@@ -1049,7 +1212,9 @@ function updateUI() {
1049
1212
  // Update recipient select (send panel) - only from coworkers.db
1050
1213
  const select = document.getElementById('recipient-select');
1051
1214
  const currentVal = select.value;
1215
+ const everyoneOption = recipients.length > 0 ? '<option value="@everyone" style="font-weight: bold; color: #5EEAD4;">@everyone (broadcast to all)</option>' : '';
1052
1216
  select.innerHTML = '<option value="">Coworker...</option>' +
1217
+ everyoneOption +
1053
1218
  recipients.sort().map(r =>
1054
1219
  `<option value="${r}" ${r === currentVal ? 'selected' : ''}>${r}</option>`
1055
1220
  ).join('');
@@ -1064,15 +1229,7 @@ function updateUI() {
1064
1229
  </div>
1065
1230
  `;
1066
1231
  } else {
1067
- messagesDiv.innerHTML = messages.slice(0, 20).map(msg => `
1068
- <div class="message-card ${msg.read ? '' : 'unread'}" data-id="${msg.id}" data-sender="${msg.sender}" data-recipient="${msg.recipient}">
1069
- <div class="message-header">
1070
- <span class="message-sender">${msg.sender} → ${msg.recipient}</span>
1071
- <span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
1072
- </div>
1073
- <div class="message-text">${marked.parse(msg.message)}</div>
1074
- </div>
1075
- `).join('');
1232
+ messagesDiv.innerHTML = messages.slice(0, 20).map(msg => renderMessageCard(msg, true)).join('');
1076
1233
 
1077
1234
  // Add click handlers for all messages (clicking marks as read and sets recipient for reply)
1078
1235
  messagesDiv.querySelectorAll('.message-card').forEach(el => {
@@ -1148,25 +1305,49 @@ async function sendMessage() {
1148
1305
  }
1149
1306
 
1150
1307
  try {
1151
- const response = await fetch('/api/send', {
1152
- method: 'POST',
1153
- headers: { 'Content-Type': 'application/json' },
1154
- body: JSON.stringify({ to, from: config.user, message })
1155
- });
1308
+ let sendPromises;
1156
1309
 
1157
- if (response.ok) {
1310
+ if (to === '@everyone') {
1311
+ // Send to all recipients individually
1312
+ sendPromises = recipients.map(recipient =>
1313
+ fetch('/api/send', {
1314
+ method: 'POST',
1315
+ headers: { 'Content-Type': 'application/json' },
1316
+ body: JSON.stringify({ to: recipient, from: config.user, message })
1317
+ })
1318
+ );
1319
+ } else {
1320
+ // Send to single recipient
1321
+ sendPromises = [fetch('/api/send', {
1322
+ method: 'POST',
1323
+ headers: { 'Content-Type': 'application/json' },
1324
+ body: JSON.stringify({ to, from: config.user, message })
1325
+ })];
1326
+ }
1327
+
1328
+ const responses = await Promise.all(sendPromises);
1329
+ const allSuccessful = responses.every(r => r.ok);
1330
+
1331
+ if (allSuccessful) {
1158
1332
  // Clear input
1159
1333
  document.getElementById('message-input').value = '';
1160
1334
 
1161
1335
  // Show toast
1162
1336
  const toast = document.getElementById('toast');
1337
+ const toastMsg = document.getElementById('toast-message');
1338
+ if (toastMsg && to === '@everyone') {
1339
+ toastMsg.textContent = `Message broadcast to ${recipients.length} coworkers!`;
1340
+ }
1163
1341
  toast.classList.add('show');
1164
- setTimeout(() => toast.classList.remove('show'), 3000);
1342
+ setTimeout(() => {
1343
+ toast.classList.remove('show');
1344
+ if (toastMsg) toastMsg.textContent = 'Message sent!';
1345
+ }, 3000);
1165
1346
 
1166
1347
  // Reload data
1167
1348
  loadData();
1168
1349
  } else {
1169
- alert('Failed to send message');
1350
+ alert('Failed to send message to some recipients');
1170
1351
  }
1171
1352
  } catch (err) {
1172
1353
  console.error('Error sending:', err);
@@ -1333,15 +1514,7 @@ function updateDeskDialogContent() {
1333
1514
  </div>
1334
1515
  `;
1335
1516
  } else {
1336
- content.innerHTML = filteredMessages.map(msg => `
1337
- <div class="message-card ${msg.read ? '' : 'unread'}" style="margin-bottom: 12px;">
1338
- <div class="message-header">
1339
- <span class="message-sender">${msg.sender} → ${msg.recipient}</span>
1340
- <span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
1341
- </div>
1342
- <div class="message-text">${marked.parse(msg.message)}</div>
1343
- </div>
1344
- `).join('');
1517
+ content.innerHTML = filteredMessages.map(msg => renderMessageCard(msg, true)).join('');
1345
1518
  }
1346
1519
  }
1347
1520
 
@@ -1428,6 +1601,123 @@ function updateDeskLabels() {
1428
1601
  });
1429
1602
  }
1430
1603
 
1604
+ // Parse markdown frontmatter from message text
1605
+ function parseFrontmatter(text) {
1606
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
1607
+ const match = text.match(frontmatterRegex);
1608
+
1609
+ if (!match) {
1610
+ return { content: text, frontmatter: null };
1611
+ }
1612
+
1613
+ const frontmatterText = match[1];
1614
+ const content = text.slice(match[0].length).trim();
1615
+
1616
+ // Simple YAML-like parsing
1617
+ const frontmatter = {};
1618
+ const lines = frontmatterText.split('\n');
1619
+
1620
+ for (const line of lines) {
1621
+ const colonIndex = line.indexOf(':');
1622
+ if (colonIndex > 0) {
1623
+ const key = line.slice(0, colonIndex).trim();
1624
+ let value = line.slice(colonIndex + 1).trim();
1625
+
1626
+ // Handle arrays: choices: ["option1", "option2"]
1627
+ if (value.startsWith('[') && value.endsWith(']')) {
1628
+ try {
1629
+ value = JSON.parse(value.replace(/'/g, '"'));
1630
+ } catch {
1631
+ // Fallback: parse as comma-separated
1632
+ value = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
1633
+ }
1634
+ } else if (value.startsWith('- ')) {
1635
+ // YAML array format with dashes - collect all consecutive dash items
1636
+ // This is handled below by checking the whole frontmatter
1637
+ } else {
1638
+ // Remove quotes if present
1639
+ value = value.replace(/^["']|["']$/g, '');
1640
+ }
1641
+
1642
+ frontmatter[key] = value;
1643
+ }
1644
+ }
1645
+
1646
+ // Handle YAML array format: choices:\n - option1\n - option2
1647
+ const choicesMatch = frontmatterText.match(/choices:\s*\n((?:\s*-\s*[^\n]+\n?)+)/);
1648
+ if (choicesMatch) {
1649
+ const choicesLines = choicesMatch[1].trim().split('\n');
1650
+ frontmatter.choices = choicesLines
1651
+ .map(line => line.replace(/^\s*-\s*/, '').trim())
1652
+ .filter(line => line)
1653
+ .map(choice => choice.replace(/^["']|["']$/g, ''));
1654
+ }
1655
+
1656
+ return { content, frontmatter };
1657
+ }
1658
+
1659
+ // Send a quick response from a choice button
1660
+ window.sendQuickResponse = async function(to, message, messageId) {
1661
+ try {
1662
+ const response = await fetch('/api/send', {
1663
+ method: 'POST',
1664
+ headers: { 'Content-Type': 'application/json' },
1665
+ body: JSON.stringify({ to, from: config.user, message })
1666
+ });
1667
+
1668
+ if (response.ok) {
1669
+ // Mark the original message as read if messageId is provided
1670
+ if (messageId) {
1671
+ await fetch(`/api/messages/${messageId}/read`, { method: 'POST' });
1672
+ }
1673
+
1674
+ // Show toast
1675
+ const toast = document.getElementById('toast');
1676
+ const toastMsg = document.getElementById('toast-message');
1677
+ if (toastMsg) toastMsg.textContent = 'Quick reply sent!';
1678
+ toast.classList.add('show');
1679
+ setTimeout(() => {
1680
+ toast.classList.remove('show');
1681
+ if (toastMsg) toastMsg.textContent = 'Message sent!';
1682
+ }, 2000);
1683
+
1684
+ // Reload data
1685
+ loadData();
1686
+ } else {
1687
+ console.error('Failed to send quick response');
1688
+ }
1689
+ } catch (err) {
1690
+ console.error('Error sending quick response:', err);
1691
+ }
1692
+ }
1693
+
1694
+ // Render a message card HTML with optional choice buttons
1695
+ function renderMessageCard(msg, showChoices = true) {
1696
+ const { content, frontmatter } = parseFrontmatter(msg.message);
1697
+ const choices = frontmatter?.choices;
1698
+ const showChoicesButtons = showChoices && Array.isArray(choices) && choices.length > 0;
1699
+ const replyTo = msg.recipient.toLowerCase() === config.user.toLowerCase() ? msg.sender : msg.recipient;
1700
+
1701
+ return `
1702
+ <div class="message-card ${msg.read ? '' : 'unread'}" data-id="${msg.id}" data-sender="${msg.sender}" data-recipient="${msg.recipient}">
1703
+ <div class="message-header">
1704
+ <span class="message-sender">${msg.sender} → ${msg.recipient}</span>
1705
+ <span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
1706
+ </div>
1707
+ <div class="message-text">${marked.parse(content)}</div>
1708
+ ${showChoicesButtons ? `
1709
+ <div class="message-choices">
1710
+ ${choices.map((choice) => `
1711
+ <button class="choice-btn" onclick="sendQuickResponse('${replyTo}', '${choice.replace(/'/g, "\\'")}', ${msg.id})">
1712
+ ${choice}
1713
+ </button>
1714
+ `).join('')}
1715
+ </div>
1716
+ ` : ''}
1717
+ </div>
1718
+ `;
1719
+ }
1720
+
1431
1721
  // Event listeners
1432
1722
  document.getElementById('send-btn').addEventListener('click', sendMessage);
1433
1723
 
package/public/index.html CHANGED
@@ -592,6 +592,18 @@
592
592
  .empty-state p {
593
593
  font-size: 1rem;
594
594
  }
595
+
596
+ /* Choice buttons on mobile */
597
+ .message-choices {
598
+ gap: 6px;
599
+ margin-top: 10px;
600
+ padding-top: 10px;
601
+ }
602
+
603
+ .choice-btn {
604
+ padding: 10px 14px;
605
+ font-size: 0.9rem;
606
+ }
595
607
  }
596
608
 
597
609
  /* Extra small screens */
@@ -776,6 +788,49 @@
776
788
  .tab-badge[style*="display: none"] {
777
789
  display: none;
778
790
  }
791
+
792
+ /* Message choice buttons */
793
+ .message-choices {
794
+ display: flex;
795
+ flex-wrap: wrap;
796
+ gap: 8px;
797
+ margin-top: 12px;
798
+ padding-top: 12px;
799
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
800
+ }
801
+
802
+ .choice-btn {
803
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
804
+ color: white;
805
+ border: none;
806
+ border-radius: 20px;
807
+ padding: 8px 16px;
808
+ font-size: 0.85rem;
809
+ font-weight: 500;
810
+ cursor: pointer;
811
+ transition: all 0.2s ease;
812
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
813
+ }
814
+
815
+ .choice-btn:hover {
816
+ transform: translateY(-2px);
817
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
818
+ background: linear-gradient(135deg, #7b8ff0 0%, #8b5eb5 100%);
819
+ }
820
+
821
+ .choice-btn:active {
822
+ transform: translateY(0);
823
+ box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
824
+ }
825
+
826
+ /* Darker variant for unread messages */
827
+ .message-card.unread .choice-btn {
828
+ background: linear-gradient(135deg, #4fd1c5 0%, #667eea 100%);
829
+ }
830
+
831
+ .message-card.unread .choice-btn:hover {
832
+ background: linear-gradient(135deg, #5ee4d8 0%, #7b8ff0 100%);
833
+ }
779
834
  </style>
780
835
  <!-- Markdown parser -->
781
836
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
@@ -841,7 +896,7 @@
841
896
  </div>
842
897
  </div>
843
898
 
844
- <div class="toast" id="toast">Message sent!</div>
899
+ <div class="toast" id="toast"><span id="toast-message">Message sent!</span></div>
845
900
 
846
901
  <script type="importmap">
847
902
  {