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 +1 -1
- package/public/app.js +321 -31
- package/public/index.html +56 -1
package/package.json
CHANGED
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
|
-
|
|
952
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(() =>
|
|
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
|
{
|