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.
- package/CHANGELOG.md +57 -0
- package/README.md +62 -21
- package/cli.js +16 -9
- package/conversation-templates/code-review.json +11 -0
- package/conversation-templates/debug-squad.json +11 -0
- package/conversation-templates/feature-build.json +11 -0
- package/conversation-templates/managed-team.json +12 -0
- package/conversation-templates/research-write.json +11 -0
- package/dashboard.html +7389 -5720
- package/dashboard.js +2017 -1766
- package/mods/built-in-accessories.json +122 -0
- package/mods/registry.json +4 -0
- package/office/accessories.js +265 -0
- package/office/agents.js +376 -0
- package/office/animation.js +337 -0
- package/office/appearance.js +56 -0
- package/office/character.js +208 -0
- package/office/constants.js +62 -0
- package/office/environment.js +805 -0
- package/office/face.js +258 -0
- package/office/hair.js +183 -0
- package/office/index.js +337 -0
- package/office/mod-loader.js +257 -0
- package/office/monitors.js +113 -0
- package/office/outfits.js +212 -0
- package/office/scene.js +75 -0
- package/office/spectator-camera.js +177 -0
- package/office/state.js +25 -0
- package/package.json +58 -56
- package/server.js +2704 -2196
- package/templates/managed.json +26 -0
package/office/agents.js
ADDED
|
@@ -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
|
+
}
|