let-them-talk 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ import { S } from './state.js';
2
+ import { DESK_POSITIONS, SPAWN_POS } from './constants.js';
3
+ import { createCharacter } from './character.js';
4
+ import { resolveAppearance } from './appearance.js';
5
+ import { buildHair } from './hair.js';
6
+ import { buildFaceSprite } from './face.js';
7
+ import { buildOutfit, removeOutfit } from './outfits.js';
8
+
9
+ export function walkTo(agent, tx, tz, callback) {
10
+ var dx = tx - agent.pos.x;
11
+ var dz = tz - agent.pos.z;
12
+ var dist = Math.sqrt(dx * dx + dz * dz);
13
+ agent.walkStart = { x: agent.pos.x, z: agent.pos.z };
14
+ agent.target = { x: tx, z: tz, cb: callback || null };
15
+ agent.walkProgress = 0;
16
+ agent.walkDuration = Math.max(dist * 0.4, 0.3);
17
+ }
18
+
19
+ export function showBubble(agent, text) {
20
+ var display = text.length > 80 ? text.substring(0, 77) + '...' : text;
21
+ agent.parts.bubbleDiv.textContent = display;
22
+ agent.parts.bubbleDiv.style.display = 'block';
23
+ agent.parts.bubbleDiv.style.opacity = '1';
24
+ agent.bubbleTimer = 4;
25
+ agent.bubbleText = display;
26
+ }
27
+
28
+ function assignDesk(agentName) {
29
+ var used = {};
30
+ for (var n in S.agents3d) used[S.agents3d[n].deskIdx] = true;
31
+ for (var i = 0; i < DESK_POSITIONS.length; i++) {
32
+ if (!used[i]) return i;
33
+ }
34
+ return Object.keys(S.agents3d).length % DESK_POSITIONS.length;
35
+ }
36
+
37
+ function fetchTasks() {
38
+ var base = window.currentProjectPath ? '/api/tasks?project=' + encodeURIComponent(window.currentProjectPath) : '/api/tasks';
39
+ fetch(base).then(function(r) { return r.json(); }).then(function(data) {
40
+ S.cachedTasks = Array.isArray(data) ? data : (data.tasks || []);
41
+ }).catch(function() {});
42
+ }
43
+
44
+ function getAgentTask(agentName) {
45
+ for (var i = 0; i < S.cachedTasks.length; i++) {
46
+ var t = S.cachedTasks[i];
47
+ if (t.assignee === agentName || t.assigned_to === agentName) return t;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function updateConversationVelocity() {
53
+ var history = window.cachedHistory;
54
+ if (!history || history.length === 0) { S.conversationVelocity = 0; return; }
55
+ var now = Date.now();
56
+ var cutoff30s = now - 30000;
57
+ var cutoff2m = now - 120000;
58
+ var recent30 = 0, recent2m = 0;
59
+ for (var i = history.length - 1; i >= 0; i--) {
60
+ var ts = new Date(history[i].timestamp).getTime();
61
+ if (ts > cutoff30s) recent30++;
62
+ if (ts > cutoff2m) recent2m++;
63
+ if (ts <= cutoff2m) break;
64
+ }
65
+ S.conversationVelocity = recent30 >= 3 ? 1 : (recent2m === 0 ? -1 : 0);
66
+ }
67
+
68
+ function updateLabel(agent) {
69
+ var nameEl = agent.parts.labelDiv.querySelector('.office3d-label-name');
70
+ var dotEl = agent.parts.labelDiv.querySelector('.office3d-label-dot');
71
+ if (nameEl) nameEl.textContent = agent.displayName;
72
+ if (dotEl) {
73
+ var colors = { active: '#4ade80', sleeping: '#facc15', dead: '#f87171' };
74
+ dotEl.style.background = colors[agent.state] || '#f87171';
75
+ }
76
+ }
77
+
78
+ function updateDeskScreen(deskIdx, status) {
79
+ var desk = S.deskMeshes[deskIdx];
80
+ if (!desk) return;
81
+ if (status === 'active') {
82
+ desk.screenMat.emissive.setHex(0x58a6ff);
83
+ desk.screenMat.emissiveIntensity = 0.5;
84
+ desk.screenMat.color.setHex(0x58a6ff);
85
+ } else if (status === 'sleeping') {
86
+ desk.screenMat.emissive.setHex(0x1a2744);
87
+ desk.screenMat.emissiveIntensity = 0.15;
88
+ desk.screenMat.color.setHex(0x1a2744);
89
+ } else {
90
+ desk.screenMat.emissive.setHex(0x333333);
91
+ desk.screenMat.emissiveIntensity = 0.1;
92
+ desk.screenMat.color.setHex(0x333333);
93
+ }
94
+ }
95
+
96
+ function flashDeskScreen(deskIdx) {
97
+ var desk = S.deskMeshes[deskIdx];
98
+ if (!desk) return;
99
+ desk.screenMat.emissive.setHex(0xffffff);
100
+ desk.screenMat.emissiveIntensity = 1.5;
101
+ setTimeout(function() {
102
+ desk.screenMat.emissive.setHex(0x58a6ff);
103
+ desk.screenMat.emissiveIntensity = 0.5;
104
+ }, 300);
105
+ }
106
+
107
+ function rebuildCharacterAppearance(agent) {
108
+ var a = resolveAppearance(agent.displayName, agent.appearance);
109
+ agent.parts.bodyMat.color.setHex(a.shirt_hex);
110
+ agent.parts.armMat.color.setHex(a.shirt_hex);
111
+ agent.parts.legMat.color.setHex(a.pants_hex);
112
+ agent.parts.headMat.color.setHex(a.head_hex);
113
+ agent.parts.handMat.color.setHex(a.head_hex);
114
+ agent.parts.shoeMat.color.setHex(a.shoe_hex);
115
+
116
+ // Rebuild hair
117
+ var oldHair = agent.parts.hairGroup;
118
+ agent.parts.group.remove(oldHair);
119
+ oldHair.traverse(function(c) { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); });
120
+ var newHair = buildHair(a.hair_style, a.hair_hex);
121
+ newHair.position.y = 1.05;
122
+ agent.parts.group.add(newHair);
123
+ agent.parts.hairGroup = newHair;
124
+
125
+ // Rebuild face
126
+ var oldFace = agent.parts.faceSprite;
127
+ agent.parts.head.remove(oldFace);
128
+ if (oldFace.material.map) oldFace.material.map.dispose();
129
+ oldFace.material.dispose();
130
+ var newFace = buildFaceSprite(a.eye_style, a.mouth_style, agent.state === 'sleeping');
131
+ newFace.position.set(0, 0, 0.251);
132
+ agent.parts.head.add(newFace);
133
+ agent.parts.faceSprite = newFace;
134
+
135
+ // Rebuild outfit
136
+ removeOutfit(agent.parts.group);
137
+ if (a.outfit) {
138
+ agent.parts.outfitGroup = buildOutfit(a.outfit, { shirt_color: a.shirt_color, pants_color: a.pants_color }, agent.parts.group);
139
+ } else {
140
+ agent.parts.outfitGroup = null;
141
+ }
142
+ }
143
+
144
+ export function syncAgents() {
145
+ if (!window.cachedAgents) return;
146
+
147
+ fetchTasks();
148
+ updateConversationVelocity();
149
+
150
+ for (var name in window.cachedAgents) {
151
+ var info = window.cachedAgents[name];
152
+ if (!S.agents3d[name]) {
153
+ var deskIdx = assignDesk(name);
154
+ var deskPos = DESK_POSITIONS[deskIdx] || DESK_POSITIONS[0];
155
+ var parts = createCharacter(info.display_name || name, info.appearance || {});
156
+ var agent = {
157
+ name: name,
158
+ displayName: info.display_name || name,
159
+ appearance: info.appearance || {},
160
+ parts: parts,
161
+ deskIdx: deskIdx,
162
+ deskPos: { x: deskPos.x, z: deskPos.z },
163
+ pos: { x: SPAWN_POS.x, z: SPAWN_POS.z },
164
+ target: null,
165
+ walkQueue: [],
166
+ walkProgress: 0,
167
+ walkDuration: 0,
168
+ walkStart: null,
169
+ state: info.status || 'active',
170
+ prevState: null,
171
+ registered: false,
172
+ bubbleTimer: 0,
173
+ bubbleText: '',
174
+ isSitting: false,
175
+ sittingLerp: 0,
176
+ facingTarget: 0,
177
+ zzzActive: false,
178
+ sleepTransition: 0,
179
+ spawnOpacity: 1,
180
+ deathOpacity: 1,
181
+ dying: false,
182
+ currentTask: null,
183
+ taskCelebration: 0,
184
+ isListening: !!(info.is_listening),
185
+ handRaiseTimer: 0,
186
+ waveTimer: 0,
187
+ thinkTimer: 0,
188
+ pointTimer: 0,
189
+ celebrateTimer: 0,
190
+ stretchTimer: 0,
191
+ idleGestureTimer: 5 + Math.random() * 10,
192
+ lastMessageTime: 0,
193
+ monitorTimer: 0,
194
+ location: 'desk', // 'desk', 'dressing_room', 'rest', 'walking'
195
+ };
196
+
197
+ parts.group.position.set(SPAWN_POS.x, 0, SPAWN_POS.z);
198
+ S.scene.add(parts.group);
199
+ updateLabel(agent);
200
+ S.agents3d[name] = agent;
201
+
202
+ // Registration animation
203
+ showBubble(agent, 'Checking in...');
204
+ (function(a) {
205
+ setTimeout(function() {
206
+ walkTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
207
+ a.registered = true;
208
+ showBubble(a, 'Ready to work!');
209
+ updateDeskScreen(a.deskIdx, a.state);
210
+ });
211
+ }, 800);
212
+ })(agent);
213
+ } else {
214
+ var existing = S.agents3d[name];
215
+ var newState = info.status || 'active';
216
+ var oldState = existing.state;
217
+
218
+ // Don't override local state changes (rest area sleeping, dressing room)
219
+ var isLocalOverride = existing.location === 'rest' || existing.location === 'dressing_room' || existing.location === 'walking';
220
+ if (newState !== oldState && !isLocalOverride) {
221
+ existing.prevState = oldState;
222
+ existing.state = newState;
223
+ if (newState === 'dead' && !existing.dying) {
224
+ existing.dying = true;
225
+ existing.deathOpacity = 1;
226
+ }
227
+ }
228
+
229
+ existing.displayName = info.display_name || name;
230
+ existing.isListening = !!(info.is_listening);
231
+
232
+ var task = getAgentTask(name);
233
+ if (task) {
234
+ var prevTask = existing.currentTask;
235
+ existing.currentTask = task;
236
+ if (prevTask && prevTask.status !== 'done' && task.status === 'done') {
237
+ existing.taskCelebration = 2;
238
+ existing.celebrateTimer = 1.5;
239
+ }
240
+ } else {
241
+ existing.currentTask = null;
242
+ }
243
+
244
+ var newApp = info.appearance || {};
245
+ if (JSON.stringify(newApp) !== JSON.stringify(existing.appearance)) {
246
+ existing.appearance = newApp;
247
+ rebuildCharacterAppearance(existing);
248
+ }
249
+
250
+ updateLabel(existing);
251
+ if (existing.registered) updateDeskScreen(existing.deskIdx, existing.state);
252
+ }
253
+ }
254
+
255
+ for (var n in S.agents3d) {
256
+ if (!window.cachedAgents[n]) {
257
+ var deadAgent = S.agents3d[n];
258
+ if (!deadAgent.dying) {
259
+ deadAgent.dying = true;
260
+ deadAgent.deathOpacity = 1;
261
+ deadAgent.state = 'dead';
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ export function processMessages() {
268
+ var history = window.cachedHistory;
269
+ if (!history || history.length === 0) return;
270
+
271
+ var newMsgs = history.slice(S.lastProcessedMsg);
272
+ S.lastProcessedMsg = history.length;
273
+
274
+ for (var i = 0; i < newMsgs.length; i++) {
275
+ var msg = newMsgs[i];
276
+ var from = S.agents3d[msg.from];
277
+ if (!from || !from.registered) continue;
278
+ var text = msg.content || msg.message || '';
279
+
280
+ from.lastMessageTime = Date.now();
281
+ flashDeskScreen(from.deskIdx);
282
+
283
+ // Contextual gesture based on message type
284
+ var isBC = !msg.to || msg.to === 'all';
285
+ if (isBC) {
286
+ from.waveTimer = 0.8;
287
+ } else {
288
+ from.pointTimer = 0.6;
289
+ }
290
+
291
+ if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
292
+ var target = S.agents3d[msg.to];
293
+ (function(f, t, txt) {
294
+ setTimeout(function() {
295
+ f.walkQueue = [];
296
+ // Calculate a stop point ~1.8m away from the target, facing them
297
+ var tx = t.pos.x, tz = t.pos.z;
298
+ var fx = f.pos.x, fz = f.pos.z;
299
+ var adx = tx - fx, adz = tz - fz;
300
+ var dist = Math.sqrt(adx * adx + adz * adz);
301
+ var stopDist = 1.8;
302
+ var stopX, stopZ;
303
+ if (dist > stopDist + 0.5) {
304
+ // Approach from sender's direction, stop 1.8m away
305
+ stopX = tx - (adx / dist) * stopDist;
306
+ stopZ = tz - (adz / dist) * stopDist;
307
+ } else {
308
+ // Already close — just step to the side of target's desk
309
+ stopX = tx + 1.5;
310
+ stopZ = tz;
311
+ }
312
+ walkTo(f, stopX, stopZ, function() {
313
+ // Sender faces target
314
+ var dx2 = t.pos.x - f.pos.x;
315
+ var dz2 = t.pos.z - f.pos.z;
316
+ f.facingTarget = Math.atan2(dx2, dz2);
317
+ showBubble(f, txt);
318
+
319
+ // Target turns toward sender (listener reaction)
320
+ var rdx = f.pos.x - t.pos.x;
321
+ var rdz = f.pos.z - t.pos.z;
322
+ t.facingTarget = Math.atan2(rdx, rdz);
323
+ t.isListening = true;
324
+ t._listeningTo = f.name;
325
+
326
+ setTimeout(function() {
327
+ // Sender walks back to desk
328
+ walkTo(f, f.deskPos.x, f.deskPos.z + 0.7);
329
+ // Target turns back to desk after a short delay
330
+ setTimeout(function() {
331
+ if (t._listeningTo === f.name) {
332
+ t.isListening = false;
333
+ t._listeningTo = null;
334
+ t.facingTarget = Math.PI; // face desk
335
+ }
336
+ }, 1500);
337
+ }, 4200);
338
+ });
339
+ }, 400);
340
+ })(from, target, text);
341
+ } else {
342
+ (function(f, txt) {
343
+ setTimeout(function() {
344
+ f.walkQueue = [];
345
+ walkTo(f, 0, 0, function() {
346
+ showBubble(f, txt);
347
+ // All nearby agents turn toward the broadcaster
348
+ for (var an in S.agents3d) {
349
+ var a = S.agents3d[an];
350
+ if (a.name === f.name || !a.registered || a.state !== 'active') continue;
351
+ var bdx = f.pos.x - a.pos.x;
352
+ var bdz = f.pos.z - a.pos.z;
353
+ a.facingTarget = Math.atan2(bdx, bdz);
354
+ a.isListening = true;
355
+ a._listeningTo = f.name;
356
+ }
357
+ setTimeout(function() {
358
+ walkTo(f, f.deskPos.x, f.deskPos.z + 0.7);
359
+ // All listeners turn back
360
+ setTimeout(function() {
361
+ for (var an2 in S.agents3d) {
362
+ var a2 = S.agents3d[an2];
363
+ if (a2._listeningTo === f.name) {
364
+ a2.isListening = false;
365
+ a2._listeningTo = null;
366
+ a2.facingTarget = Math.PI;
367
+ }
368
+ }
369
+ }, 1500);
370
+ }, 4200);
371
+ });
372
+ }, 400);
373
+ })(from, text);
374
+ }
375
+ }
376
+ }