@kylecheng3146/agent-ops 0.7.2 → 0.8.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/README.md +2 -0
- package/dist/packages/cli/src/args.js +16 -0
- package/dist/packages/cli/src/bin.js +40 -8
- package/dist/packages/cli/src/cli.js +2 -0
- package/dist/packages/cli/src/commands/batch.js +3 -1
- package/dist/packages/cli/src/commands/update.js +18 -0
- package/dist/packages/cli/src/commands/verify.js +3 -1
- package/dist/packages/cli/src/hook-process.js +48 -0
- package/dist/packages/cli/src/office-entry.js +34 -6
- package/dist/packages/cli/src/run-deps.js +8 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -0
- package/dist/runtime/src/install/update.js +10 -2
- package/dist/runtime/src/office/activity.js +105 -0
- package/dist/runtime/src/office/art.js +999 -0
- package/dist/runtime/src/office/collect.js +160 -11
- package/dist/runtime/src/office/layout.js +200 -0
- package/dist/runtime/src/office/page.js +760 -645
- package/dist/runtime/src/office/scene.js +27 -31
- package/dist/runtime/src/office/sessions.js +22 -6
- package/dist/runtime/src/office/snapshot.js +62 -1
- package/dist/runtime/src/parallel/finish.js +4 -2
- package/dist/runtime/src/review/batch.js +10 -0
- package/dist/runtime/src/review/execute.js +9 -0
- package/dist/runtime/src/run/hosts/codex.js +1 -1
- package/docs/en/guides/configuration.md +7 -0
- package/docs/en/spec/harness-adapters.md +16 -2
- package/docs/zh-TW/guides/configuration.md +6 -0
- package/docs/zh-TW/spec/harness-adapters.md +12 -2
- package/package.json +1 -1
|
@@ -1,280 +1,95 @@
|
|
|
1
|
+
import { ROOM_PALETTE, avatarColors, notePosition, personSprites, roomSprites } from "./art.js";
|
|
2
|
+
import { findPath, officeLayout, walkGrid } from "./layout.js";
|
|
1
3
|
import { sceneModel } from "./scene.js";
|
|
2
|
-
/** Warm cream, oak and sage colors, kept to a single 16-color pixel palette. */
|
|
3
|
-
export const PALETTE = ["#2b241f", "#355a4b", "#6d9275", "#b7c7a3", "#8a5033", "#729ead", "#d9aa72", "#ead8b8",
|
|
4
|
-
"#8e7561", "#6b5d50", "#adc39a", "#f2e5c9", "#e8c99c", "#c86f4a", "#f2c95c", "#fff7e6"];
|
|
5
|
-
/** Each person has a fixed 16-color palette, independent of the room colors. */
|
|
6
|
-
export function avatarPalette(identity, viewer = false) {
|
|
7
|
-
let hash = 0;
|
|
8
|
-
for (const character of identity)
|
|
9
|
-
hash = (Math.imul(hash, 31) + character.charCodeAt(0)) >>> 0;
|
|
10
|
-
const hair = viewer ? ["#302c3b", "#514757", "#756575"] : [
|
|
11
|
-
["#302c3b", "#514757", "#756575"], ["#51372f", "#80533d", "#b47c51"], ["#745338", "#a37c4f", "#d1ad72"]
|
|
12
|
-
][hash % 3];
|
|
13
|
-
const skin = viewer ? ["#bf8264", "#e6ae87", "#f5cda4"] : [
|
|
14
|
-
["#bf8264", "#e6ae87", "#f5cda4"], ["#9b624b", "#c08762", "#e0ae82"], ["#654039", "#96624e", "#bf8c68"]
|
|
15
|
-
][Math.floor(hash / 9) % 3];
|
|
16
|
-
const clothes = viewer ? ["#bb893b", "#e3b64f", "#f7d878"] : [
|
|
17
|
-
["#37596d", "#5687a1", "#87b3c7"], ["#934d49", "#c57060", "#e6a087"],
|
|
18
|
-
["#3b6658", "#61947b", "#92bea1"], ["#625776", "#8c7da4", "#b7a9ca"]
|
|
19
|
-
][Math.floor(hash / 27) % 4];
|
|
20
|
-
return ["#292633", ...hair, ...skin, ...clothes, "#404958", "#707a8b", "#2e3443", "#626e7f", "#fff1db", "#d58c76"];
|
|
21
|
-
}
|
|
22
|
-
/** Original 32×48 pixel people. Self-contained so the inline client uses this same art. */
|
|
23
|
-
export function avatarSprite(identity, direction = "down", pose = 0, viewer = false) {
|
|
24
|
-
let hash = 0;
|
|
25
|
-
for (const character of identity)
|
|
26
|
-
hash = (Math.imul(hash, 31) + character.charCodeAt(0)) >>> 0;
|
|
27
|
-
const hair = "2", hairLight = "3";
|
|
28
|
-
const style = viewer ? 0 : Math.floor(hash / 3) % 3;
|
|
29
|
-
const skin = "5", skinLight = "6", skinShade = "4", outfit = ["8", "7", "9"];
|
|
30
|
-
const grid = Array.from({ length: 48 }, () => Array(32).fill("."));
|
|
31
|
-
const rect = (x, y, width, height, color) => {
|
|
32
|
-
for (let row = y; row < y + height; row++)
|
|
33
|
-
for (let column = x; column < x + width; column++) {
|
|
34
|
-
if (row >= 0 && row < 48 && column >= 0 && column < 32)
|
|
35
|
-
grid[row][column] = color;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const stride = pose === 1 ? -1 : pose === 3 ? 1 : 0;
|
|
39
|
-
const side = direction === "left" || direction === "right";
|
|
40
|
-
// Slim trousers, cuffs and shaded shoes; opposite feet alternate during a walk.
|
|
41
|
-
for (let leg = 0; leg < 2; leg++) {
|
|
42
|
-
const x = side ? 12 + leg * 4 : 10 + leg * 7, lift = leg ? -stride : stride;
|
|
43
|
-
rect(x, 35 + lift, 5, 10, "0");
|
|
44
|
-
rect(x + 1, 36 + lift, 3, 7, "a");
|
|
45
|
-
rect(x + 1, 37 + lift, 1, 5, "b");
|
|
46
|
-
rect(x + 1, 43 + lift, 3, 1, "e");
|
|
47
|
-
rect(x - 1, 44 + lift, 6, 3, "c");
|
|
48
|
-
rect(x, 44 + lift, 4, 1, "d");
|
|
49
|
-
rect(x, 46 + lift, 4, 1, "e");
|
|
50
|
-
}
|
|
51
|
-
const bodyX = side ? 11 : 8, bodyW = side ? 11 : 16;
|
|
52
|
-
rect(bodyX + 2, 23, bodyW - 4, 1, "0");
|
|
53
|
-
rect(bodyX, 24, bodyW, 12, "0");
|
|
54
|
-
rect(bodyX + 1, 25, bodyW - 2, 10, outfit[0]);
|
|
55
|
-
rect(bodyX + 1, 33, bodyW - 2, 2, outfit[1]);
|
|
56
|
-
rect(bodyX + bodyW - 3, 27, 2, 6, outfit[1]);
|
|
57
|
-
rect(bodyX + 2, 27, 2, 4, outfit[2]);
|
|
58
|
-
rect(bodyX + 3, 26, bodyW - 6, 1, outfit[2]);
|
|
59
|
-
if (direction !== "up") {
|
|
60
|
-
rect(side ? 18 : 13, 22, side ? 3 : 6, 3, skinShade);
|
|
61
|
-
rect(side ? 18 : 13, 22, side ? 2 : 5, 2, skin);
|
|
62
|
-
rect(side ? 18 : 12, 25, side ? 2 : 8, 1, outfit[1]);
|
|
63
|
-
rect(side ? 18 : 13, 25, side ? 1 : 6, 1, outfit[2]);
|
|
64
|
-
if (!side) {
|
|
65
|
-
rect(18, 29, 3, 3, outfit[1]);
|
|
66
|
-
rect(18, 29, 3, 1, outfit[2]);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const arms = side ? [12] : [5, 24];
|
|
70
|
-
arms.forEach((x, index) => {
|
|
71
|
-
const swing = stride * (index ? -1 : 1);
|
|
72
|
-
rect(x, 25 + swing, 3, 10, "0");
|
|
73
|
-
rect(x + 1, 26 + swing, 2, 4, outfit[0]);
|
|
74
|
-
rect(x + 1, 29 + swing, 2, 1, outfit[1]);
|
|
75
|
-
rect(x + 1, 30 + swing, 2, 4, skinShade);
|
|
76
|
-
rect(x + 1, 30 + swing, 1, 3, skinLight);
|
|
77
|
-
});
|
|
78
|
-
// Rounded jaw and small dark eyes, with warm skin shading instead of white face blocks.
|
|
79
|
-
rect(9, 9, 14, 1, "0");
|
|
80
|
-
rect(7, 10, 18, 9, "0");
|
|
81
|
-
rect(8, 19, 16, 2, "0");
|
|
82
|
-
rect(10, 21, 12, 1, "0");
|
|
83
|
-
rect(9, 10, 14, 11, skin);
|
|
84
|
-
rect(8, 12, 16, 7, skin);
|
|
85
|
-
rect(22, 13, 2, 6, skinShade);
|
|
86
|
-
rect(19, 20, 3, 1, skinShade);
|
|
87
|
-
rect(10, 13, 3, 1, skinLight);
|
|
88
|
-
rect(6, 15, 2, 4, "0");
|
|
89
|
-
rect(24, 15, 2, 4, "0");
|
|
90
|
-
rect(7, 15, 1, 3, skin);
|
|
91
|
-
rect(24, 15, 1, 3, skin);
|
|
92
|
-
if (side) {
|
|
93
|
-
rect(23, 14, 3, 4, "0");
|
|
94
|
-
rect(23, 15, 2, 2, skin);
|
|
95
|
-
rect(21, 15, 2, 2, "0");
|
|
96
|
-
rect(21, 15, 1, 1, "e");
|
|
97
|
-
rect(23, 19, 1, 1, skinShade);
|
|
98
|
-
}
|
|
99
|
-
else if (direction === "down") {
|
|
100
|
-
rect(10, 14, 3, 1, hairLight);
|
|
101
|
-
rect(19, 14, 3, 1, hairLight);
|
|
102
|
-
rect(11, 15, 2, 2, "0");
|
|
103
|
-
rect(19, 15, 2, 2, "0");
|
|
104
|
-
rect(11, 15, 1, 1, "e");
|
|
105
|
-
rect(19, 15, 1, 1, "e");
|
|
106
|
-
rect(9, 18, 2, 1, skinShade);
|
|
107
|
-
rect(21, 18, 2, 1, skinShade);
|
|
108
|
-
rect(15, 19, 2, 1, skinShade);
|
|
109
|
-
}
|
|
110
|
-
const crown = ["00000000", "00HHHHHHHH00", "0HHHHHHHHHHHH0", "0HHHHHHHHHHHHHH0", "0HHHHhhHHHHHHHHHH0",
|
|
111
|
-
"0HHHhhhhhHHHHHHHHH0", "0HHHHHHhhhhhHHHHHHH0", "0HHHHHHHHHHHHHHHHHH0", "0HHHHHHHHHHHHHHHHHH0"];
|
|
112
|
-
crown.forEach((row, y) => [...row].forEach((pixel, x) => rect(Math.floor((32 - row.length) / 2) + x, y + 2, 1, 1, pixel === "H" ? hair : pixel === "h" ? hairLight : pixel)));
|
|
113
|
-
rect(5, 11, side ? 10 : 4, 5, hair);
|
|
114
|
-
rect(7, 11, side ? 9 : 7, 2, hair);
|
|
115
|
-
rect(11, 12, 2, 2, hair);
|
|
116
|
-
rect(14, 11, 2, 1, hair);
|
|
117
|
-
rect(6, 11, 1, 3, hairLight);
|
|
118
|
-
if (direction === "up") {
|
|
119
|
-
rect(6, 10, 20, 8, hair);
|
|
120
|
-
rect(8, 18, 16, 2, hair);
|
|
121
|
-
rect(10, 20, 12, 1, "0");
|
|
122
|
-
rect(7, 12, 1, 4, hairLight);
|
|
123
|
-
rect(22, 16, 2, 1, hairLight);
|
|
124
|
-
rect(11, 19, 10, 1, hairLight);
|
|
125
|
-
}
|
|
126
|
-
else if (!side) {
|
|
127
|
-
rect(23, 11, 3, 5, hair);
|
|
128
|
-
rect(21, 11, 4, 2, hair);
|
|
129
|
-
rect(20, 12, 2, 1, hair);
|
|
130
|
-
}
|
|
131
|
-
if (style === 1) { // Bob: longer sides and a low back, distinct from the short cut.
|
|
132
|
-
rect(5, 14, side ? 9 : 4, 8, hair);
|
|
133
|
-
if (!side)
|
|
134
|
-
rect(23, 14, 3, 8, hair);
|
|
135
|
-
if (direction === "up")
|
|
136
|
-
rect(8, 18, 16, 5, hair);
|
|
137
|
-
rect(6, 16, 1, 4, hairLight);
|
|
138
|
-
}
|
|
139
|
-
else if (style === 2) { // Curly outline with a raised crown and uneven fringe.
|
|
140
|
-
rect(8, 1, 3, 3, hair);
|
|
141
|
-
rect(14, 1, 3, 3, hair);
|
|
142
|
-
rect(21, 2, 3, 3, hair);
|
|
143
|
-
rect(5, 6, 2, 2, hairLight);
|
|
144
|
-
rect(14, 11, 2, 2, hair);
|
|
145
|
-
rect(19, 10, 2, 2, hair);
|
|
146
|
-
}
|
|
147
|
-
return grid.map(row => (direction === "left" ? row.reverse() : row).join(""));
|
|
148
|
-
}
|
|
149
|
-
/** Every sprite is a character matrix: one hex digit per palette index. */
|
|
150
|
-
export const SPRITES = {
|
|
151
|
-
person: [
|
|
152
|
-
"..................", "....8888888888....", "...888888888888...", "..888cccccc8888...",
|
|
153
|
-
"..88c0cccc0c88....", "..88cccccccc88....", "...88cccccc88.....", "....cccccccc......",
|
|
154
|
-
"...cSSSSSSSSSc....", "..cSSSSSSSSSSSc...", "..cSSSSSSSSSSSc...", "...SSSSSSSSSSSS...",
|
|
155
|
-
"....SSSSSSSSSS....", "....ccSSSScc......", ".....999999.......", "....99999999......",
|
|
156
|
-
"....99....99......", "...999....999.....", "...999....999.....", "..9999....9999...."
|
|
157
|
-
],
|
|
158
|
-
step: [
|
|
159
|
-
"..................", "....8888888888....", "...888888888888...", "..888cccccc8888...",
|
|
160
|
-
"..88c0cccc0c88....", "..88cccccccc88....", "...88cccccc88.....", "....cccccccc......",
|
|
161
|
-
"...cSSSSSSSSSc....", "..cSSSSSSSSSSSc...", "..cSSSSSSSSSSSc...", "...SSSSSSSSSSSS...",
|
|
162
|
-
"....SSSSSSSSSS....", "....ccSSSScc......", ".....999999.......", "....99999999......",
|
|
163
|
-
".....99999........", "....999999........", "...999....999.....", "..9999....9999...."
|
|
164
|
-
],
|
|
165
|
-
desk: [
|
|
166
|
-
"..........88888888..............", "..........81111118..............", "..........81bb1b18..............",
|
|
167
|
-
"..........81111118..............", "..........88888888..............", ".............8888...............",
|
|
168
|
-
"00000000000000000000000000000000", "0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0", "06666666666666666666666666666660",
|
|
169
|
-
"06666666666666666666666666666660", "00000000000000000000000000000000", "060..........................060",
|
|
170
|
-
"060..........................060", "060..........................060", "060..........................060", "000..........................000"
|
|
171
|
-
],
|
|
172
|
-
whiteboard: [
|
|
173
|
-
"00000000000000000000000000000000", "0ffffffffffffffffffffffffffffff0", "0ff11f1111fffff44f4fffff22fffff0",
|
|
174
|
-
"0ffffffffffffffffffffffffffffff0", "0ff1111f11ff44444fffff2222fffff0", "0ffffffffffffffffffffffffffffff0",
|
|
175
|
-
"0ff11111ffff444fffff22fffffffff0", "0ffffffffffffffffffffffffffffff0", "0ffffffffffffffffffffffffffffff0",
|
|
176
|
-
"00000000000000000000000000000000", "0777777777777777777777777777777.", "..............0880..............",
|
|
177
|
-
"..............0880..............", "..............0880..............", "..............0880..............", "............00000000............"
|
|
178
|
-
],
|
|
179
|
-
bench: [
|
|
180
|
-
"......b.........a.........d.....", ".....0b0.......0a0.......0d0....", ".....0b0.......0a0.......0d0....",
|
|
181
|
-
"....0bbb0.....0aaa0.....0ddd0...", "...0bbbbb0...0aaaaa0...0ddddd0..", "...0000000...0000000...0000000..",
|
|
182
|
-
"00000000000000000000000000000000", "0ffffffffffffffffffffffffffffff0", "07777777777777777777777777777770",
|
|
183
|
-
"00000000000000000000000000000000", "070888888888888888888888888880.0", "070..........................070",
|
|
184
|
-
"070..........................070", "070..........................070", "070..........................070", "000..........................000"
|
|
185
|
-
],
|
|
186
|
-
table: [
|
|
187
|
-
"................................", "....000000000000000000000000....", "..0066666666666666666666666600..",
|
|
188
|
-
".06666666666666666666666666666..", "0666666ffff666666666ffff6666660.", "0666666ffff666666666ffff6666660.",
|
|
189
|
-
"0666666666666666666666666666660.", ".066666666666666666666666666660.", "..0066666666666666666666666600..",
|
|
190
|
-
"....000000000000000000000000....", "......060..............060......", "......060..............060......",
|
|
191
|
-
"......060..............060......", "......060..............060......", "......000..............000......", "................................"
|
|
192
|
-
],
|
|
193
|
-
rug: [
|
|
194
|
-
"000000000000000000000000", "0aaaaaaaaaaaaaaaaaaaaaa0", "0a22222222222222222222a0", "0a22222222222222222222a0",
|
|
195
|
-
"0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0",
|
|
196
|
-
"0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0",
|
|
197
|
-
"0a22222222222222222222a0", "0a22222222222222222222a0", "0aaaaaaaaaaaaaaaaaaaaaa0", "000000000000000000000000"
|
|
198
|
-
],
|
|
199
|
-
chair: [
|
|
200
|
-
"....000000....", "...06666660...", "...06666660...", "...06666660...", "....000000....", "....066660....",
|
|
201
|
-
"....066660....", "....066660....", "....000000....", "...000..000...", "...060..060...", "...060..060...",
|
|
202
|
-
"..0000..0000..", ".............."
|
|
203
|
-
],
|
|
204
|
-
window: [
|
|
205
|
-
"000000000000000000000000", "0bbbbbbbbbbbbbbbbbbbbbb0", "0bffffffffffffffffffffb0", "0bffffffff0000ffffffffb0",
|
|
206
|
-
"0bffffffff0000ffffffffb0", "0bffffffffffffffffffffb0", "0bffffffffffffffffffffb0", "000000000000000000000000"
|
|
207
|
-
],
|
|
208
|
-
door: [
|
|
209
|
-
"0000000000000000", "0666666666666660", "0644444444444460", "0646666664666460", "0646666664666460",
|
|
210
|
-
"0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460", "06466666646ee460",
|
|
211
|
-
"06466666646ee460", "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460",
|
|
212
|
-
"0646666664666460", "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460",
|
|
213
|
-
"0646666664666460", "0644444444444460", "0666666666666660", "0000000000000000", "0aaaaaaaaaaaaaa0",
|
|
214
|
-
"0a2a2a2a2a2a2aa0", "0aaaaaaaaaaaaaa0", "0000000000000000"
|
|
215
|
-
],
|
|
216
|
-
shelf: [
|
|
217
|
-
"00000000000000000000000000000000", "06666666666666666666666666666660", "06............................60",
|
|
218
|
-
"06............................60", "06............................60", "06............................60", "06............................60",
|
|
219
|
-
"06............................60", "06............................60", "06666666666666666666666666666660", "06............................60",
|
|
220
|
-
"06............................60", "06............................60", "06............................60", "06............................60",
|
|
221
|
-
"06............................60", "06............................60", "06666666666666666666666666666660", "00000000000000000000000000000000"
|
|
222
|
-
],
|
|
223
|
-
book: ["SSS", "SfS", "SSS", "SSS", "SSS", "SSS", "SSS"],
|
|
224
|
-
clock: [
|
|
225
|
-
".....000000.....", "...00ffffff00...", "..0ffff0ffff0...", ".0fffffffffff0..", ".0fffffffffff0..",
|
|
226
|
-
"0ffffffffffffff0", "0f0ffffffffff0f0", "0ffffffffffffff0", "0ffffffffffffff0", ".0fffffffffff0..",
|
|
227
|
-
".0fffffffffff0..", "..0ffff0ffff0...", "...00ffffff00...", ".....000000.....", "................", "................"
|
|
228
|
-
],
|
|
229
|
-
plant: [
|
|
230
|
-
"......a..a......", "...a..aa.a..a...", "....aa2aa2aa....", "..a.2aa22aa2.a..", "...aa2a22a2aa...",
|
|
231
|
-
"....2aa22aa2....", ".....2a22a2.....", "......2222......", ".....000000.....", ".....066660.....",
|
|
232
|
-
".....064460.....", "......0660......", "......0660......", "......0000......", "................", "................"
|
|
233
|
-
],
|
|
234
|
-
paper: ["000000", "0ffff0", "000000"],
|
|
235
|
-
alert: [".00.", "0ee0", "0ee0", "0ee0", "0ee0", ".00.", "0ee0", ".00."],
|
|
236
|
-
floor: [
|
|
237
|
-
"7777777777777777", "6666666666666666", "666666b666666666", "66666666666b6666", "7777777777777777",
|
|
238
|
-
"6666666666666666", "6666b66666666666", "6666666666666666", "7777777777777777", "6666666666666666",
|
|
239
|
-
"6666666666666b66", "6666666666666666", "7777777777777777", "666666b666666666", "6666666666666666",
|
|
240
|
-
"6666666666666666"
|
|
241
|
-
],
|
|
242
|
-
wall: [
|
|
243
|
-
"7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
|
|
244
|
-
"7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
|
|
245
|
-
"7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
|
|
246
|
-
"7777777777777777"
|
|
247
|
-
],
|
|
248
|
-
baseboard: ["bbbbbbbbbbbbbbbb", "0000000000000000"]
|
|
249
|
-
};
|
|
250
4
|
const STYLE = String.raw `
|
|
251
5
|
:root{color-scheme:light}
|
|
252
6
|
*{box-sizing:border-box}
|
|
253
7
|
html,body{width:100%;height:100%;overflow:hidden}
|
|
254
|
-
body{margin:0;background:#2b241f;color:#2b241f;font:
|
|
8
|
+
body{margin:0;background:#2b241f;color:#2b241f;font:14px/1.45 system-ui,-apple-system,sans-serif}
|
|
255
9
|
button{font:inherit;color:inherit}
|
|
256
10
|
#app{height:100dvh;display:flex;flex-direction:column;overflow:hidden;background:#ead8b8}
|
|
257
11
|
#header{min-height:54px;display:flex;align-items:center;gap:12px;padding:8px 18px;background:#2b241f;color:#fff7e6;border-bottom:4px solid #8a5033;white-space:nowrap}
|
|
258
|
-
#brand{font
|
|
12
|
+
#brand{font:bold clamp(16px,1.4vw,22px)/1.25 "Courier New",monospace;letter-spacing:.02em}
|
|
259
13
|
#crumb{flex:1;overflow:hidden;text-overflow:ellipsis;color:#f2e5c9}
|
|
260
|
-
#header button{border:2px solid #f2e5c9;background:#
|
|
14
|
+
#header button{border:2px solid #f2e5c9;background:#355a4b;color:#fff7e6;padding:5px 9px;cursor:pointer;box-shadow:2px 2px 0 #000}
|
|
261
15
|
#header button:hover,#header button:focus-visible{background:#adc39a;color:#2b241f;outline:3px solid #f2c95c;outline-offset:2px}
|
|
262
16
|
#header button[hidden],#header select[hidden]{display:none}
|
|
263
17
|
#header select{border:2px solid #f2e5c9;background:#355a4b;color:#fff7e6;padding:4px 6px;font:inherit;box-shadow:2px 2px 0 #000}
|
|
264
18
|
#header select:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
|
|
265
19
|
#live{color:#adc39a}
|
|
266
|
-
#
|
|
267
|
-
#
|
|
20
|
+
#panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
|
|
21
|
+
#panel-toggle{flex:none;border:2px solid #2b241f;background:#d9aa72;padding:2px 8px;font-size:13px;cursor:pointer;box-shadow:2px 2px 0 #8a5033}
|
|
22
|
+
#panel-toggle:hover,#panel-toggle:focus-visible{background:#f2c95c;outline:3px solid #355a4b;outline-offset:2px}
|
|
23
|
+
#workspace.collapsed{grid-template-columns:minmax(0,1fr) 46px}
|
|
24
|
+
#workspace.collapsed #work-panel{padding:6px 4px;align-items:center}
|
|
25
|
+
#workspace.collapsed #work-heading,#workspace.collapsed #work-summary,#workspace.collapsed #work-list{display:none}
|
|
26
|
+
#workspace.collapsed #panel-head{flex:1}
|
|
27
|
+
#workspace.collapsed #panel-toggle{writing-mode:vertical-rl;padding:10px 4px;letter-spacing:.1em}
|
|
28
|
+
#workspace.collapsed #wrap{overflow:hidden}
|
|
29
|
+
#workspace{min-height:0;flex:1;display:grid;grid-template-columns:minmax(0,1fr) clamp(330px,28vw,400px);gap:12px;padding:10px}
|
|
30
|
+
#wrap{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;background:#ead8b8}
|
|
31
|
+
#overview{display:flex;flex-direction:column;gap:18px;padding:4px 8px 16px;outline:none}
|
|
32
|
+
#overview[hidden],#room-view[hidden]{display:none}
|
|
33
|
+
.repo-head{display:flex;align-items:baseline;gap:10px;margin:0 0 8px;padding:4px 10px;font-size:16px;background:#2b241f;color:#fff7e6;border-left:6px solid #6d9275}
|
|
34
|
+
.repo-alert{color:#f2c95c;font-size:14px}
|
|
35
|
+
.cards{display:flex;flex-wrap:wrap;gap:14px;justify-content:flex-start}
|
|
36
|
+
.card{position:relative;display:block;max-width:100%;padding:0;border:0;background:transparent;text-align:left;cursor:pointer}
|
|
37
|
+
.card-caption{position:absolute;left:10px;bottom:10px;max-width:calc(100% - 20px);display:flex;flex-wrap:wrap;align-items:baseline;gap:0 10px;padding:3px 10px;background:rgba(43,36,31,.85);border:2px solid #2b241f;pointer-events:none}
|
|
38
|
+
.card canvas{display:block;max-width:100%;height:auto;image-rendering:pixelated;image-rendering:crisp-edges;border:4px solid #6b5d50;box-shadow:4px 4px 0 #8a5033}
|
|
39
|
+
.card:hover canvas,.card:focus-visible canvas{border-color:#355a4b;outline:3px solid #f2c95c;outline-offset:2px}
|
|
40
|
+
.card:focus-visible{outline:none}
|
|
41
|
+
.card-title{font-weight:bold;color:#fff7e6;overflow-wrap:anywhere}
|
|
42
|
+
.card-meta{font-size:13px;color:#f2e5c9}
|
|
43
|
+
.empty-room{cursor:default}
|
|
44
|
+
.page-grid{display:grid;justify-content:center;gap:14px}
|
|
45
|
+
#overview.paged{height:100%;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none;padding:0;gap:0}
|
|
46
|
+
#overview.paged::-webkit-scrollbar{display:none}
|
|
47
|
+
.page{scroll-snap-align:start;scroll-snap-stop:always;display:flex;align-items:center;justify-content:center;padding:8px 16px}
|
|
48
|
+
#room-view{display:flex;flex-direction:column;align-items:center;gap:6px;padding:4px 8px}
|
|
49
|
+
#hud{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;width:100%;padding:6px 10px;background:#fff7e6;border:2px solid #8a5033}
|
|
50
|
+
.hud-title{font-size:15px;margin-right:4px;overflow-wrap:anywhere}
|
|
51
|
+
.hud-bar{display:inline-block;width:110px;height:10px;background:#e2cda6;border:2px solid #2b241f}
|
|
52
|
+
.hud-bar i{display:block;height:100%;background:#6d9a5a}
|
|
53
|
+
.hud-count{font-variant-numeric:tabular-nums}
|
|
54
|
+
.badge{padding:0 6px;font-size:12px;border:2px solid #6b5d50;background:#f2e5c9}
|
|
55
|
+
.badge.good{border-color:#3f6b4e;color:#3f6b4e}
|
|
56
|
+
.badge.bad{border-color:#c86f4a;color:#a2502f}
|
|
57
|
+
.badge.alert{background:#c86f4a;border-color:#2b241f;color:#fff7e6;cursor:pointer}
|
|
58
|
+
#stage{position:relative}
|
|
59
|
+
#office{display:block;image-rendering:pixelated;image-rendering:crisp-edges;border:4px solid #6b5d50;box-shadow:6px 6px 0 #8a5033;outline:none;cursor:pointer;box-sizing:content-box}
|
|
268
60
|
#office:focus-visible{outline:4px solid #f2c95c;outline-offset:4px}
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
-
|
|
61
|
+
#labels{position:absolute;left:4px;top:4px;pointer-events:none}
|
|
62
|
+
#labels span{position:absolute;white-space:nowrap;font-size:12px;line-height:1.3}
|
|
63
|
+
#labels span[hidden]{display:none}
|
|
64
|
+
.zone-label{padding:0 6px;background:rgba(43,36,31,.82);color:#fff7e6;font-weight:bold}
|
|
65
|
+
.nameplate{transform:translateX(-50%);padding:0 4px;background:#fff7e6;border:1px solid #6b5d50;color:#2b241f}
|
|
66
|
+
.nameplate.alert{border-color:#c86f4a;color:#a2502f;font-weight:bold}
|
|
67
|
+
.nameplate.viewer{background:#f2c95c}
|
|
68
|
+
.overflow{padding:0 5px;background:#355a4b;color:#fff7e6;font-weight:bold}
|
|
69
|
+
.speech{transform:translate(-50%,-100%);max-width:260px;white-space:normal!important;padding:4px 8px;background:#fff7e6;border:2px solid #2b241f;box-shadow:2px 2px 0 #8a5033;color:#2b241f}
|
|
70
|
+
#work-panel{min-width:0;min-height:0;display:flex;flex-direction:column;padding:12px;background:#fff7e6;border:3px solid #6b5d50;box-shadow:4px 4px 0 #8a5033}
|
|
71
|
+
#work-heading{margin:0;font-size:18px;color:#355a4b}
|
|
72
|
+
#work-summary{margin:4px 0 10px;font-size:13px}
|
|
73
|
+
#work-list{min-height:0;overflow:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:8px;padding:2px 4px 4px 0}
|
|
74
|
+
.work-card{flex-shrink:0;padding:10px;border:2px solid #b7c7a3;background:#f2e5c9;overflow-wrap:anywhere}
|
|
75
|
+
.work-card.attention{border-color:#8a5033;border-left-width:5px}
|
|
76
|
+
.work-card button{display:block;width:100%;text-align:left;white-space:normal;overflow-wrap:anywhere;border:0;background:transparent;padding:0;cursor:pointer}
|
|
77
|
+
.work-name{font-weight:bold;font-size:14px;text-decoration:underline;text-underline-offset:3px;color:#355a4b}
|
|
78
|
+
.work-card p{margin:4px 0 0}
|
|
79
|
+
.criterion{border-left-width:5px}
|
|
80
|
+
.criterion.pass{border-color:#6d9275}
|
|
81
|
+
.criterion.fail{border-color:#c86f4a}
|
|
82
|
+
.criterion.unknown{border-color:#6b5d50}
|
|
83
|
+
.criterion.pending{border-color:#d9aa72}
|
|
84
|
+
.work-section{margin:6px 0 0;font-size:14px;color:#355a4b}
|
|
85
|
+
.work-command{display:block;padding:4px 6px;background:#f2e5c9;font:12px/1.4 monospace;overflow-wrap:anywhere}
|
|
86
|
+
.work-meta,.work-proof{font-size:13px;color:#4b4035}
|
|
87
|
+
.work-action,.work-reason{font-size:13px;font-weight:600;color:#8a5033}
|
|
88
|
+
.work-card button:focus-visible{outline:3px solid #355a4b;outline-offset:3px}
|
|
274
89
|
#status{position:fixed;z-index:4;inset:6dvh 50% auto auto;transform:translateX(50%);width:min(92vw,680px);max-height:88dvh;overflow:hidden;display:flex;flex-direction:column;gap:4px;padding:16px;background:#fff7e6;border:4px solid #6b5d50;box-shadow:8px 8px 0 #2b241f;color:#2b241f}
|
|
275
90
|
#status[hidden]{display:none}
|
|
276
|
-
#status h2{margin:0 0 8px;color:#355a4b;font-size:1.25em}
|
|
277
|
-
#status .dialog-body{min-height:0;overflow:
|
|
91
|
+
#status h2{margin:0 0 8px;color:#355a4b;font-size:1.25em;overflow-wrap:anywhere}
|
|
92
|
+
#status .dialog-body{min-height:0;overflow:auto}
|
|
278
93
|
#status p{margin:5px 0;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
279
94
|
#status ul{margin:5px 0;padding-left:22px}
|
|
280
95
|
#status li{overflow-wrap:anywhere}
|
|
@@ -284,29 +99,72 @@ button{font:inherit;color:inherit}
|
|
|
284
99
|
#status .dialog-pager{border-top:2px solid #d9aa72;padding-top:6px}
|
|
285
100
|
#status button{border:2px solid #2b241f;background:#d9aa72;padding:5px 10px;cursor:pointer}
|
|
286
101
|
#status button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
|
|
102
|
+
#status.panel h2{font-family:"Courier New",monospace}
|
|
103
|
+
.panel-heading{margin:10px 0 4px;font-size:14px;color:#355a4b}
|
|
104
|
+
.panel-meta{font-size:13px;color:#4b4035}
|
|
105
|
+
.panel-phases{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 8px;padding:0;list-style:none}
|
|
106
|
+
.panel-phases li{padding:2px 8px;border:2px solid #6b5d50;background:#f2e5c9;font-size:13px}
|
|
107
|
+
.panel-phases li.now{background:#355a4b;color:#fff7e6;border-color:#2b241f}
|
|
108
|
+
.panel-notes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
|
|
109
|
+
.panel-column{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
110
|
+
.note{display:flex;flex-direction:column;gap:2px;padding:6px 8px;border:2px solid #2b241f;font-size:13px;box-shadow:2px 2px 0 rgba(43,36,31,.35);overflow-wrap:anywhere}
|
|
111
|
+
.note.todo{background:#f2c95c}.note.fail{background:#f3c2b4;border-color:#c86f4a}.note.pass{background:#cfe0a4}
|
|
112
|
+
.panel-question{padding:6px 8px;border:2px solid #c86f4a;background:#fdeee8}
|
|
113
|
+
#status.panel-task{border-color:#9c8d7d;background:#fffdf7}
|
|
114
|
+
#status.panel-diff{background:#2f3f4f;color:#e8eef0;border-color:#2b241f}
|
|
115
|
+
#status.panel-diff h2,#status.panel-diff .panel-meta{color:#a9d3d8}
|
|
116
|
+
.panel-files{margin:6px 0;padding-left:18px;font:13px/1.5 monospace}
|
|
117
|
+
.panel-files li.recent{color:#f2c95c}
|
|
118
|
+
#status.panel-verify{background:#2b241f;color:#fff7e6;border-color:#2f3f4f}
|
|
119
|
+
#status.panel-verify h2,#status.panel-verify .panel-meta{color:#a8c46a}
|
|
120
|
+
.panel-leds{list-style:none;margin:6px 0;padding:0;display:flex;flex-direction:column;gap:8px}
|
|
121
|
+
.led-row{display:flex;flex-direction:column;gap:2px;padding:6px 8px 6px 14px;border-left:6px solid #6b5d50;background:#3a2f28}
|
|
122
|
+
.led-row.pass{border-color:#a8c46a}.led-row.fail,.led-row.unknown{border-color:#c86f4a}
|
|
123
|
+
.panel-output{margin:4px 0 0;padding:6px 8px;max-height:220px;overflow:auto;background:#1f1a16;color:#f2e5c9;font:12px/1.4 monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
124
|
+
#status.panel-verify code,#status.panel-diff code{background:#1f1a16;color:#fff7e6}
|
|
125
|
+
#status.panel-review{background:#f0cf93;border-color:#2b241f}
|
|
126
|
+
.stamp{display:inline-block;margin:0 0 6px;padding:2px 10px;border:3px solid currentColor;font:bold 16px "Courier New",monospace;transform:rotate(-4deg)}
|
|
127
|
+
.stamp.pass{color:#3f6b4e}.stamp.fail{color:#a2502f}.stamp.not_run{color:#6b5d50}
|
|
128
|
+
.round{margin:8px 0;padding:8px 10px;background:#fff7e6;border:2px solid #b8743f}
|
|
129
|
+
.panel-findings{margin:6px 0 0;padding-left:18px}
|
|
130
|
+
.panel-findings li{margin:4px 0}.panel-findings li.blocking strong{color:#a2502f}
|
|
131
|
+
.panel-findings .advice{color:#355a4b}
|
|
132
|
+
#status.panel-integration{background:#e8c38c;border-color:#2b241f}
|
|
133
|
+
.panel-facts{display:grid;grid-template-columns:max-content 1fr;gap:2px 12px;margin:0;padding:8px;background:#fff7e6;border:2px solid #2b241f}
|
|
134
|
+
.panel-facts dt{font-weight:bold}.panel-facts dd{margin:0;font-family:monospace;overflow-wrap:anywhere}
|
|
135
|
+
.panel-steps{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 0;padding:0;list-style:none}
|
|
136
|
+
.panel-steps li{padding:2px 10px;border:2px solid #6b5d50;background:#fff7e6;font-size:13px}
|
|
137
|
+
.panel-steps li.done{border-color:#3f6b4e;color:#3f6b4e}.panel-steps li.failed{border-color:#c86f4a;color:#a2502f}
|
|
138
|
+
@media(max-width:640px){.panel-notes{grid-template-columns:1fr}}
|
|
287
139
|
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
140
|
+
@media(max-width:1000px){#workspace{grid-template-columns:1fr;grid-template-rows:minmax(240px,1fr) minmax(180px,.8fr);gap:8px;padding:6px}#workspace.collapsed{grid-template-columns:1fr;grid-template-rows:1fr auto}#workspace.collapsed #panel-toggle{writing-mode:horizontal-tb;padding:2px 8px}#work-panel{padding:8px}#work-heading{font-size:16px}}
|
|
288
141
|
@media(max-width:760px){#header{flex-wrap:wrap;gap:6px;padding:6px 8px}#brand{font-size:16px}#header button{order:1;font-size:12px;padding:3px 5px}#crumb{min-width:70px}#live{font-size:12px}}
|
|
289
142
|
@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
|
|
290
143
|
`;
|
|
291
144
|
const CLIENT = String.raw `
|
|
292
|
-
var PALETTE = __PALETTE__, SPRITES = __SPRITES__,
|
|
293
|
-
var sceneModel = __SCENE__;
|
|
294
|
-
var
|
|
295
|
-
var
|
|
145
|
+
var PALETTE = __PALETTE__, SPRITES = __SPRITES__, PEOPLE = __PEOPLE__, LAYOUT = __LAYOUT__;
|
|
146
|
+
var sceneModel = __SCENE__, avatarColors = __AVATAR_COLORS__, walkGrid = __WALK__, findPath = __PATH__, notePosition = __NOTE_POS__;
|
|
147
|
+
var GRID = walkGrid(LAYOUT), ROOM_W = LAYOUT.width, ROOM_H = LAYOUT.height, BOARD_SLOTS = 12;
|
|
148
|
+
var ROOM_OF = {planning:"planning", implementing:"implementing", verifying:"verifying", reviewing:"reviewing", integrating:"integrating"};
|
|
149
|
+
var app = document.getElementById("app"), canvas = document.getElementById("office"), ctx = canvas.getContext("2d");
|
|
296
150
|
ctx.imageSmoothingEnabled = false;
|
|
297
|
-
var
|
|
151
|
+
var wrap = document.getElementById("wrap"), overviewBox = document.getElementById("overview"), roomView = document.getElementById("room-view"), hud = document.getElementById("hud"), labels = document.getElementById("labels");
|
|
152
|
+
var statusBox = document.getElementById("status");
|
|
153
|
+
// The right-hand list can fold away so the office takes the whole width; the choice is a per-viewer convenience.
|
|
154
|
+
var workspace = document.getElementById("workspace"), panelToggle = document.getElementById("panel-toggle"), panelCollapsed = readCollapsed();
|
|
155
|
+
function readCollapsed(){ try { return localStorage.getItem("agent-office-list") === "collapsed"; } catch (_) { return false; } }
|
|
156
|
+
var workList=document.getElementById('work-list'),workHeading=document.getElementById('work-heading'),workSummary=document.getElementById('work-summary'),workSignature='';
|
|
298
157
|
var crumb = document.getElementById("crumb"), backButton = document.getElementById("back"), recentButton = document.getElementById("recent");
|
|
299
158
|
var languageButton = document.getElementById("language"), live = document.getElementById("live"), repoSelect = document.getElementById("repo-filter"), repoFilter = "", repoSignature = "";
|
|
300
|
-
var snapshot = null, model = null,
|
|
159
|
+
var snapshot = null, model = null, frame = 0, offline = false, scale = 1, scaleY = 1;
|
|
301
160
|
var mode = "overview", selectedKey = null, showRecent = false, detailPage = 0, dialogClose = null, detailPreviousFocus = null, detailTarget = null;
|
|
302
161
|
var detailSignature = "";
|
|
303
|
-
var
|
|
304
|
-
var supervisor = {x:43, y:19, targetX:43, targetY:19}, supervisorReady = false, hallwayReturn = null;
|
|
305
|
-
var roomSupervisor = {x:32, y:34, targetX:32, targetY:34};
|
|
162
|
+
var people = {}, staffMemory = {}, viewer = spawnViewer(), doorFrames = {}, images = {}, bases = {}, roomCanvases = {}, overviewSignature = "", hudSignature = "", labelNodes = {}, lastFigures = [], stageCanvas = null;
|
|
306
163
|
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
164
|
+
function spawnViewer(){ return {x: Math.round(LAYOUT.spawn.x / 2) * 2, y: Math.round(LAYOUT.spawn.y / 2) * 2, path: [], dir: "up", step: 0}; }
|
|
307
165
|
var STRINGS = {
|
|
308
|
-
en: {viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", repos:"Repository", allRepos:"All repositories", noTask:"No task", noTaskHint:"Nothing to verify or review", back:"Back to overview", connected:"● Connected", offline:"○ Reconnecting", quiet:"The office is quiet. No agent is at work.", waiting:"waiting for your answer", enter:"Enter room", close:"Close", previous:"Previous", next:"Next", page:"Page", details:"Details", progress:"Progress", verify:"Verify", review:"Review", pending:"Awaiting answer", task:"Task", status:"Status", phase:"Phase", host:"Host", now:"Now", owner:"Owner", currentWork:"Current work", question:"Pending question", questions:"Questions", workList:"Work list", clickWork:"Click for work", keyboardWork:"Press L for work list", keyboardPhase:"Press 1-5 for phase work", keyboardQuestions:"Press Q for questions", noPeople:"No known work is in this phase", noQuestions:"No pending questions", files:"Changed files", commands:"Commands", copy:"Copy", copied:"Copied", unknown:"unknown", unassigned:"unassigned", phases:{planning:"Planning", implementing:"Implementing", verifying:"Verifying", reviewing:"Reviewing", integrating:"Integrating", unknown:"Unassigned"}, statuses:{active:"active", idle:"idle", running:"running", reviewing:"reviewing", verifying:"verifying", blocked:"blocked", delivered:"delivered", complete:"complete", unknown:"unknown", unassigned:"unassigned", pending:"pending"}},
|
|
309
|
-
zh: {viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", repos:"專案", allRepos:"所有專案", noTask:"無任務", noTaskHint:"沒有需要驗證或審查的工作", back:"返回總覽", connected:"● 已連線", offline:"○ 重新連線中", quiet:"辦公室很安靜,目前沒有成員工作。", waiting:"等待你的回覆", enter:"進入房間", close:"關閉", previous:"上一頁", next:"下一頁", page:"頁", details:"詳細資料", progress:"進度", verify:"驗證", review:"審查", pending:"待回覆", task:"任務", status:"狀態", phase:"階段", host:"主機", now:"目前", owner:"負責人", currentWork:"目前工作", question:"待回覆問題", questions:"問題", workList:"工作清單", clickWork:"點擊查看工作", keyboardWork:"按 L 開啟工作清單", keyboardPhase:"按 1-5 查看階段工作", keyboardQuestions:"按 Q 查看問題", noPeople:"此階段目前沒有已知工作", noQuestions:"目前沒有待回覆問題", files:"變更檔案", commands:"指令", copy:"複製", copied:"已複製", unknown:"未知", unassigned:"未分配", phases:{planning:"規劃", implementing:"開發", verifying:"驗證", reviewing:"審查", integrating:"整合", unknown:"未分配"}, statuses:{active:"進行中", idle:"閒置", running:"工作中", reviewing:"審查中", verifying:"驗證中", blocked:"受阻", delivered:"已交付", complete:"已完成", unknown:"未知", unassigned:"未分配", pending:"待回覆"}}
|
|
166
|
+
en: {copyFailed:"Copy manually", copyHint:"Clipboard unavailable; select the command and copy it manually.", checks:"Acceptance checks", unfinished:"Not complete", sessionEnded:"Session ended", attention:"Needs attention", verifyFailed:"Verification failed", reviewFailed:"Review failed", blockedUnknown:"Blocked; reason not provided", openQuestion:"View questions and commands", detailsAction:"Details and commands", stale:"Reconnecting; showing last known state", viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", repos:"Repository", allRepos:"All repositories", noTask:"No task", noTaskHint:"Nothing to verify or review", panel_task:"Whiteboard", panel_diff:"Screen", panel_verify:"QA board", panel_review:"Review desk", panel_integration:"Sorting table", noChanges:"No changed files yet.", filesCount:"files", notReviewed:"Not reviewed yet.", firstRound:"Round 1", recheck:"Re-check", blocking:"blocking", refutedYes:"The re-check overturned round 1.", refutedNo:"The re-check upheld round 1.", worktree:"Worktree", branch:"Branch", base:"Base", ahead:"Commits ahead", commitStep:"Commit", finishStep:"worktree finish (merge)", hideList:"Hide list »", showList:"« Work list", moveHint:"Arrow keys or WASD walk; Enter talks to the nearest person; Esc returns.", critPass:"PASS", critFail:"FAIL", critUnknown:"Undetermined", critPending:"Not verified yet", noCriteria:"This room has no acceptance criteria.", back:"Back to overview", connected:"● Connected", offline:"○ Reconnecting", quiet:"The office is quiet. No agent is at work.", integrator:"Integrator", staff_verify:"Running verification", staff_review:"Reviewing", staff_wait:"Waiting for the other round", staff_finish:"Merging the worktree", staff_rest:"Resting in the lobby", waiting:"waiting for your answer", enter:"Enter room", close:"Close", previous:"Previous", next:"Next", page:"Page", details:"Details", progress:"Progress", verify:"Verify", review:"Review", pending:"Awaiting answer", task:"Task", status:"Status", phase:"Phase", host:"Host", now:"Now", owner:"Owner", currentWork:"Current work", question:"Pending question", questions:"Questions", workList:"Work list", clickWork:"Click for work", keyboardWork:"Press L for work list", keyboardPhase:"Press 1-5 for phase work", keyboardQuestions:"Press Q for questions", noPeople:"No known work is in this phase", noQuestions:"No pending questions", files:"Changed files", commands:"Commands", copy:"Copy", copied:"Copied", unknown:"unknown", unassigned:"unassigned", phases:{planning:"Planning", implementing:"Implementing", verifying:"Verifying", reviewing:"Reviewing", integrating:"Integrating", lobby:"Lobby", unknown:"Unassigned"}, statuses:{active:"active", idle:"idle", running:"running", reviewing:"reviewing", verifying:"verifying", blocked:"blocked", delivered:"delivered", complete:"complete", unknown:"unknown", unassigned:"unassigned", pending:"pending"}},
|
|
167
|
+
zh: {copyFailed:"手動複製", copyHint:"無法存取剪貼簿;請選取指令後手動複製。", checks:"驗收條件", unfinished:"尚未完成", sessionEnded:"工作階段已結束", attention:"需要介入", verifyFailed:"驗證未通過", reviewFailed:"審查未通過", blockedUnknown:"受阻;尚無原因資料", openQuestion:"查看問題與指令", detailsAction:"詳情與指令", stale:"重新連線中;顯示最後已知狀態", viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", repos:"專案", allRepos:"所有專案", noTask:"無任務", noTaskHint:"沒有需要驗證或審查的工作", panel_task:"白板", panel_diff:"隔間螢幕", panel_verify:"QA 狀態看板", panel_review:"主管桌", panel_integration:"分信桌", noChanges:"目前沒有變更的檔案。", filesCount:"個檔案", notReviewed:"尚未審查。", firstRound:"第 1 輪", recheck:"對抗複查", blocking:"阻擋", refutedYes:"對抗複查推翻了第 1 輪。", refutedNo:"對抗複查維持第 1 輪的結論。", worktree:"Worktree", branch:"分支", base:"基準", ahead:"領先 commit", commitStep:"commit", finishStep:"worktree finish(合併)", hideList:"收合 »", showList:"« 工作清單", moveHint:"方向鍵或 WASD 走動;Enter 和最近的人對話;Esc 返回。", critPass:"通過", critFail:"未通過", critUnknown:"無法判定", critPending:"還沒驗證", noCriteria:"這間房沒有驗收條件。", back:"返回總覽", connected:"● 已連線", offline:"○ 重新連線中", quiet:"辦公室很安靜,目前沒有成員工作。", integrator:"整合", staff_verify:"執行驗證中", staff_review:"審查中", staff_wait:"等待另一輪審查", staff_finish:"合併 worktree 中", staff_rest:"在大廳休息", waiting:"等待你的回覆", enter:"進入房間", close:"關閉", previous:"上一頁", next:"下一頁", page:"頁", details:"詳細資料", progress:"進度", verify:"驗證", review:"審查", pending:"待回覆", task:"任務", status:"狀態", phase:"階段", host:"主機", now:"目前", owner:"負責人", currentWork:"目前工作", question:"待回覆問題", questions:"問題", workList:"工作清單", clickWork:"點擊查看工作", keyboardWork:"按 L 開啟工作清單", keyboardPhase:"按 1-5 查看階段工作", keyboardQuestions:"按 Q 查看問題", noPeople:"此階段目前沒有已知工作", noQuestions:"目前沒有待回覆問題", files:"變更檔案", commands:"指令", copy:"複製", copied:"已複製", unknown:"未知", unassigned:"未分配", phases:{planning:"規劃", implementing:"開發", verifying:"驗證", reviewing:"審查", integrating:"整合", lobby:"大廳", unknown:"未分配"}, statuses:{active:"進行中", idle:"閒置", running:"工作中", reviewing:"審查中", verifying:"驗證中", blocked:"受阻", delivered:"已交付", complete:"已完成", unknown:"未知", unassigned:"未分配", pending:"待回覆"}}
|
|
310
168
|
};
|
|
311
169
|
function getLanguage(){
|
|
312
170
|
try { var cookie = document.cookie.split(";").map(function(part){ return part.trim().split("="); }).find(function(pair){ return pair[0] === "agent-office-language"; }); if (cookie && (cookie[1] === "en" || cookie[1] === "zh")) return cookie[1]; } catch (_) {}
|
|
@@ -332,56 +190,10 @@ function localizeText(value){
|
|
|
332
190
|
function saveLanguage(){ try { localStorage.setItem("agent-office-language", lang); } catch (_) {} try { document.cookie = "agent-office-language=" + lang + "; Max-Age=31536000; Path=/; SameSite=Strict"; } catch (_) {} }
|
|
333
191
|
function safeText(value, fallback){ return typeof value === "string" && value.length ? value : (fallback || t("unknown")); }
|
|
334
192
|
function short(value, limit){ value = safeText(value); limit = limit || 22; return value.length > limit ? value.slice(0, limit - 1) + "…" : value; }
|
|
335
|
-
function
|
|
336
|
-
var rows = SPRITES[name]; if (!rows) return;
|
|
337
|
-
var scale = name === "floor" || name === "wall" || name === "baseboard" ? 1 : S;
|
|
338
|
-
for (var r = 0; r < rows.length; r++) for (var c = 0; c < rows[r].length; c++) {
|
|
339
|
-
var ch = rows[r][c]; if (ch === ".") continue;
|
|
340
|
-
ctx.fillStyle = ch === "S" ? shirt : PALETTE[parseInt(ch, 16)]; ctx.fillRect(x + c * scale, y + r * scale, scale, scale);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
function avatarScale(surface){return Math.max(1,Math.round(Math.min(surface.width/(model.roomCols*T),surface.height/(model.roomRows*T))*T/9));}
|
|
344
|
-
function advanceAvatar(state, tx, ty, speed){
|
|
345
|
-
var dx=tx-state.x,dy=ty-state.y;
|
|
346
|
-
if(Math.abs(dx)>.05||Math.abs(dy)>.05)state.direction=Math.abs(dx)>Math.abs(dy)?(dx<0?'left':'right'):(dy<0?'up':'down');
|
|
347
|
-
if(reducedMotion){state.x=tx;state.y=ty;}else{state.x+=Math.max(-speed,Math.min(speed,dx));state.y+=Math.max(-speed,Math.min(speed,dy));}
|
|
348
|
-
state.walking=!reducedMotion&&(Math.abs(state.x-tx)>.05||Math.abs(state.y-ty)>.05);
|
|
349
|
-
}
|
|
350
|
-
function drawAvatar(identity, state, x, y, scale, viewer){
|
|
351
|
-
var pose=state.walking&&!reducedMotion?Math.floor(frame/7)%4:0,direction=state.direction||'down',key=JSON.stringify([identity,direction,pose,!!viewer]);
|
|
352
|
-
var image=avatarImages[key];
|
|
353
|
-
if(!image){
|
|
354
|
-
image=document.createElement('canvas');image.width=32;image.height=48;var paint=image.getContext('2d'),rows=avatarSprite(identity,direction,pose,!!viewer),colors=avatarPalette(identity,!!viewer);
|
|
355
|
-
for(var row=0;row<rows.length;row++)for(var col=0;col<rows[row].length;col++){var pixel=rows[row][col];if(pixel!=='.'){paint.fillStyle=colors[parseInt(pixel,16)];paint.fillRect(col,row,1,1);}}
|
|
356
|
-
avatarImages[key]=image;
|
|
357
|
-
}
|
|
358
|
-
x=Math.round(x);y=Math.round(y);
|
|
359
|
-
ctx.fillStyle='rgba(43,36,31,.18)';ctx.fillRect(x+8*scale,y+46*scale,17*scale,2*scale);
|
|
360
|
-
ctx.drawImage(image,x,y,32*scale,48*scale);
|
|
361
|
-
}
|
|
362
|
-
function propSize(kind){ var s = SPRITES[kind] || [""]; return {w:s[0].length * S, h:s.length * S}; }
|
|
363
|
-
function roomMetrics(width,height){var rx=width/model.roomCols,ry=height/model.roomRows;return {rx:rx,ry:ry,unit:Math.max(.5,Math.min(rx,ry)/8)};}
|
|
364
|
-
function propGeometry(prop,width,height){
|
|
365
|
-
var metrics=roomMetrics(width,height),rawX=prop.x*metrics.rx,rawY=prop.y*metrics.ry;
|
|
366
|
-
// layout sizes keep the existing scale decisions; visual sizes follow the
|
|
367
|
-
// actual rounded pixel art drawn by furniture() so clicks never drift away.
|
|
368
|
-
var layoutWidths={window:58,plant:32,shelf:28,door:24},visualWidths={desk:75,bench:75,table:65,window:58,plant:32,shelf:28,door:24,whiteboard:65,chair:21},heights={desk:45,bench:45,table:54,window:31,whiteboard:49,shelf:61,plant:31,chair:32,door:46};
|
|
369
|
-
var visualHeights={desk:45,bench:49,table:54,window:34,plant:34,shelf:61,door:46,whiteboard:49,chair:32},visualXOffsets={window:-3},visualYOffsets={bench:-4,window:-3,plant:-3};
|
|
370
|
-
var layoutWidth=layoutWidths[prop.kind]||75,visualWidth=visualWidths[prop.kind]||layoutWidth,artHeight=heights[prop.kind]||45;
|
|
371
|
-
var rawScale=Math.max(.3,Math.min(metrics.unit*1.85,(height-rawY-2*metrics.unit)/artHeight,(width-rawX-2*metrics.unit)/layoutWidth)),scale=Math.max(1,Math.round(rawScale)),visualHeight=visualHeights[prop.kind]||artHeight,visualY=Math.round(rawY+(visualYOffsets[prop.kind]||0)*scale);
|
|
372
|
-
return {x:Math.round(rawX+(visualXOffsets[prop.kind]||0)*scale),y:visualY,rawX:rawX,rawY:rawY,width:visualWidth*scale,height:visualHeight*scale,scale:scale,rawScale:rawScale,unit:metrics.unit};
|
|
373
|
-
}
|
|
374
|
-
function phaseAreaGeometry(area,width,height){var metrics=roomMetrics(width,height);return {x:area.x*metrics.rx,y:area.y*metrics.ry,width:area.width*metrics.rx,height:area.height*metrics.ry,rx:metrics.rx,ry:metrics.ry,unit:metrics.unit};}
|
|
375
|
-
function progressBoardGeometry(width,height){var metrics=roomMetrics(width,height);return {x:24*metrics.rx,y:metrics.ry,width:27*metrics.rx,height:Math.max(7*metrics.ry,66*metrics.unit),unit:metrics.unit};}
|
|
376
|
-
function pendingBoardGeometry(width,height){var board=progressBoardGeometry(width,height),inset=4*board.unit;return {x:board.x+inset,y:board.y+50*board.unit,width:Math.max(0,board.width-inset*2),height:Math.max(0,Math.min(15*board.unit,board.height-50*board.unit)),unit:board.unit};}
|
|
377
|
-
function surfaceForFloor(floor){if(mode==='room')return {x:0,y:0,width:canvas.width,height:canvas.height};var box=roomBox(floor);return {x:Math.round(box.x*T),y:Math.round(box.y*T),width:Math.round(box.width*T),height:Math.round(box.height*T)};}
|
|
378
|
-
function canvasPoint(event){var rect=canvas.getBoundingClientRect();return {x:(event.clientX-rect.left)*canvas.width/rect.width,y:(event.clientY-rect.top)*canvas.height/rect.height};}
|
|
379
|
-
function inside(point,rect){return point.x>=rect.x&&point.x<rect.x+rect.width&&point.y>=rect.y&&point.y<rect.y+rect.height;}
|
|
380
|
-
function fillText(value, x, y, size, color){ ctx.fillStyle = color || PALETTE[0]; ctx.font = "bold " + Math.max(5, Math.round((size || 10) * U)) + "px monospace"; ctx.textBaseline = "top"; ctx.fillText(short(value, 36), x, y); }
|
|
193
|
+
function shortName(value,limit){value=safeText(value);return value.length>limit?value.slice(0,limit-6)+'…'+value.slice(-5):value;}
|
|
381
194
|
function roomByKey(key){ if (!model) return null; for (var i = 0; i < model.floors.length; i++) if (model.floors[i].key === key) return model.floors[i]; return null; }
|
|
382
195
|
function visibleRooms(){ if (!model) return []; return model.floors.filter(function(f){ return (showRecent || !f.completedAt) && (!repoFilter || f.repo === repoFilter); }); }
|
|
383
196
|
function repoNames(){ var names = []; if (model) model.floors.forEach(function(f){ if (f.repo && names.indexOf(f.repo) < 0) names.push(f.repo); }); return names; }
|
|
384
|
-
function repoColor(name){ var hash = 0; for (var i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) >>> 0; return PALETTE[[1, 4, 9, 13, 0, 8][hash % 6]] || PALETTE[1]; }
|
|
385
197
|
function renderRepoFilter(){
|
|
386
198
|
var names = repoNames(); if (repoFilter && names.indexOf(repoFilter) < 0) repoFilter = "";
|
|
387
199
|
repoSelect.hidden = names.length < 2; repoSelect.setAttribute("aria-label", t("repos"));
|
|
@@ -390,271 +202,472 @@ function renderRepoFilter(){
|
|
|
390
202
|
}
|
|
391
203
|
function activeRoom(){ return selectedKey ? roomByKey(selectedKey) : null; }
|
|
392
204
|
function hasRecent(){ return !!(model && model.floors.some(function(f){ return !!f.completedAt; })); }
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
205
|
+
// ---- art: sprites become small canvases once; the canvas world draws no text ----
|
|
206
|
+
function mirror(rows){ return rows.map(function(row){ return row.split("").reverse().join(""); }); }
|
|
207
|
+
function rasterize(rows, colours){
|
|
208
|
+
var image = document.createElement("canvas"); image.width = rows[0].length; image.height = rows.length;
|
|
209
|
+
var g = image.getContext("2d"); g.imageSmoothingEnabled = false;
|
|
210
|
+
for (var y = 0; y < rows.length; y++) {
|
|
211
|
+
var row = rows[y], x = 0;
|
|
212
|
+
while (x < row.length) { var key = row[x], end = x + 1; while (end < row.length && row[end] === key) end++; if (key !== "." && colours[key]) { g.fillStyle = colours[key]; g.fillRect(x, y, end - x, 1); } x = end; }
|
|
213
|
+
}
|
|
214
|
+
return image;
|
|
215
|
+
}
|
|
216
|
+
function spriteImage(name){ if (!images[name]) images[name] = rasterize(SPRITES[name], PALETTE); return images[name]; }
|
|
217
|
+
function personImage(id, role, pose){
|
|
218
|
+
var key = JSON.stringify([id, role, pose]);
|
|
219
|
+
if (!images[key]) images[key] = rasterize(pose.indexOf("left") === 0 ? mirror(PEOPLE["right" + pose.slice(4)]) : (PEOPLE[pose] || PEOPLE.down0), avatarColors(id, role));
|
|
220
|
+
return images[key];
|
|
221
|
+
}
|
|
222
|
+
function makeCanvas(){ var c = document.createElement("canvas"); c.width = ROOM_W; c.height = ROOM_H; c.getContext("2d").imageSmoothingEnabled = false; return c; }
|
|
223
|
+
function fill(g, key, r){ g.fillStyle = PALETTE[key]; g.fillRect(r.x, r.y, r.w, r.h); }
|
|
224
|
+
function tile(g, name, r){
|
|
225
|
+
var image = spriteImage(name), w = SPRITES[name][0].length, h = SPRITES[name].length;
|
|
226
|
+
g.save(); g.beginPath(); g.rect(r.x, r.y, r.w, r.h); g.clip();
|
|
227
|
+
for (var y = r.y; y < r.y + r.h; y += h) for (var x = r.x; x < r.x + r.w; x += w) g.drawImage(image, x, y);
|
|
228
|
+
g.restore();
|
|
229
|
+
}
|
|
230
|
+
function verifyFails(floor){ return floor.board.verify === "FAIL"; }
|
|
231
|
+
function variantName(name, variant, floor){ return variant ? name + (verifyFails(floor) ? "Fail" : "Pass") : name; }
|
|
232
|
+
/** Floors, walls and wall decor: drawn once per room and verification state. */
|
|
233
|
+
function roomBase(floor){
|
|
234
|
+
var key = floor.key + "|" + verifyFails(floor);
|
|
235
|
+
if (bases[key]) return bases[key];
|
|
236
|
+
var base = makeCanvas(), g = base.getContext("2d");
|
|
237
|
+
fill(g, "2", {x: 0, y: 0, w: ROOM_W, h: ROOM_H});
|
|
238
|
+
LAYOUT.rooms.forEach(function(room){ tile(g, room.floor, room); });
|
|
239
|
+
LAYOUT.rugs.forEach(function(rug){ fill(g, rug.border, rug); fill(g, rug.fill, {x: rug.x + 3, y: rug.y + 3, w: rug.w - 6, h: rug.h - 6}); });
|
|
240
|
+
tile(g, "wall", LAYOUT.walls.back); tile(g, "wall", LAYOUT.walls.middle);
|
|
241
|
+
LAYOUT.walls.vertical.forEach(function(v){ fill(g, "1", v); fill(g, "2", {x: v.x + 1, y: v.y, w: 6, h: v.h}); fill(g, "3", {x: v.x + 1, y: v.y, w: 2, h: v.h}); fill(g, "4", {x: v.x + 1, y: v.y, w: 1, h: v.h}); });
|
|
242
|
+
LAYOUT.walls.sides.concat([LAYOUT.walls.bottom]).forEach(function(r){ fill(g, "2", r); });
|
|
243
|
+
var door = LAYOUT.entrance; fill(g, "7", door); fill(g, "b", {x: door.x + 2, y: door.y - 8, w: door.w - 4, h: 8}); fill(g, "c", {x: door.x + 4, y: door.y - 6, w: door.w - 8, h: 4});
|
|
244
|
+
LAYOUT.decor.forEach(function(d){ g.drawImage(spriteImage(variantName(d.sprite, d.variant, floor)), d.x, d.y); });
|
|
245
|
+
bases[key] = base;
|
|
246
|
+
return base;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---- people: a slot in the room of their phase; they walk only when the phase changes ----
|
|
250
|
+
/** Engineers plan and build; once the work is with QA, the reviewers or the integrator, they stay at their cubicle. */
|
|
251
|
+
function roomFor(actor){ var room = ROOM_OF[actor.phase] || "lobby"; return room === "verifying" || room === "reviewing" || room === "integrating" ? "implementing" : room; }
|
|
252
|
+
// ---- resident staff: they rest in the lobby and walk to their station while their real process runs ----
|
|
253
|
+
var STAFF_ROLES = ["qa", "reviewerA", "reviewerB", "integrator"], STAFF_FADE = 600;
|
|
254
|
+
function activityOf(floor, kind){ var list = floor.activities || []; for (var i = 0; i < list.length; i++) if (list[i].kind === kind) return list[i]; return null; }
|
|
255
|
+
/** Reviewer nameplates: the running review's targets, else the newest report's rounds, else A and B. */
|
|
256
|
+
function reviewerNames(floor){
|
|
257
|
+
var review = activityOf(floor, "review"), source = sourceFor(floor), report = source && source.review, names = ["A", "B"];
|
|
258
|
+
if (review && review.targets.length === 2) names = review.targets.slice();
|
|
259
|
+
else if (report && report.rounds && report.rounds.length === 2) names = report.rounds.map(function(round){ return safeText(round.target); });
|
|
260
|
+
return names[0] === names[1] ? [names[0] + " 1", names[1] + " 2"] : names;
|
|
261
|
+
}
|
|
262
|
+
function staffResult(floor, kind){
|
|
263
|
+
var value = kind === "verify" ? floor.board.verify : kind === "review" ? floor.board.review : floor.completedAt ? "PASS" : "FAIL";
|
|
264
|
+
return value === "PASS" ? "pass" : value === "FAIL" ? "fail" : null;
|
|
265
|
+
}
|
|
266
|
+
function staffOf(floor){
|
|
267
|
+
var names = reviewerNames(floor), review = activityOf(floor, "review"), second = !!(review && review.round === 2), at = LAYOUT.staff.stations;
|
|
268
|
+
var plans = {qa: ["verify", "QA", "qa", at.qa, "verify"], reviewerA: ["review", names[0], "reviewer", second ? at.armchair : at.desk, "review"],
|
|
269
|
+
reviewerB: ["review", names[1], "reviewer", second ? at.desk : at.armchair, "review"], integrator: ["finish", t("integrator"), "integrator", at.integrator, "integration"]};
|
|
270
|
+
return STAFF_ROLES.map(function(role, index){
|
|
271
|
+
var plan = plans[role], busy = !!activityOf(floor, plan[0]), slot = busy ? plan[3] : LAYOUT.staff.rest[index];
|
|
272
|
+
var narration = !busy ? t("staff_rest") : plan[0] !== "review" ? t("staff_" + plan[0]) : slot === at.desk ? t("staff_review") : t("staff_wait");
|
|
273
|
+
return {key: floor.key + "|staff:" + role, id: plan[1], kind: plan[2], staff: role, activity: plan[0], panel: plan[4], busy: busy, slot: slot,
|
|
274
|
+
target: slot.feet.x + "," + slot.feet.y, narration: narration, questionCount: 0, status: busy ? "running" : "idle"};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** Sends one person towards a slot; they walk only when the target changes, and jump under reduced motion. */
|
|
278
|
+
function moveTo(key, slot, target, hidden){
|
|
279
|
+
var state = people[key];
|
|
280
|
+
if (!state) { people[key] = {x: slot.feet.x, y: slot.feet.y, path: [], dir: "down", step: 0, target: target, slot: slot, hidden: hidden}; return; }
|
|
281
|
+
state.hidden = hidden;
|
|
282
|
+
if (state.target === target) return;
|
|
283
|
+
state.target = target; state.slot = slot;
|
|
284
|
+
var path = reducedMotion ? null : findPath(GRID, {x: state.x, y: state.y}, slot.feet);
|
|
285
|
+
if (path && path.length > 1) state.path = path.slice(1);
|
|
286
|
+
else { state.path = []; state.x = slot.feet.x; state.y = slot.feet.y; }
|
|
287
|
+
}
|
|
288
|
+
function layoutRoom(id){ for (var i = 0; i < LAYOUT.rooms.length; i++) if (LAYOUT.rooms[i].id === id) return LAYOUT.rooms[i]; return LAYOUT.rooms[LAYOUT.rooms.length - 1]; }
|
|
289
|
+
function placements(floor){
|
|
290
|
+
var counts = {}, byActor = {};
|
|
291
|
+
floor.actors.slice().sort(function(a, b){ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; }).forEach(function(actor){
|
|
292
|
+
var room = roomFor(actor), index = counts[room] || 0; counts[room] = index + 1; byActor[actor.key] = {room: room, index: index};
|
|
293
|
+
});
|
|
294
|
+
return {byActor: byActor, counts: counts};
|
|
295
|
+
}
|
|
296
|
+
function syncPeople(){
|
|
297
|
+
if (!model) return;
|
|
298
|
+
var seen = {};
|
|
299
|
+
model.floors.forEach(function(floor){
|
|
300
|
+
var place = placements(floor).byActor;
|
|
301
|
+
floor.actors.forEach(function(actor){
|
|
302
|
+
var p = place[actor.key], room = layoutRoom(p.room);
|
|
303
|
+
seen[actor.key] = true;
|
|
304
|
+
moveTo(actor.key, room.slots[Math.min(p.index, room.slots.length - 1)], p.room + ":" + p.index, p.index >= room.slots.length);
|
|
305
|
+
});
|
|
306
|
+
staffOf(floor).forEach(function(member){
|
|
307
|
+
var memory = staffMemory[member.key];
|
|
308
|
+
// A finished process leaves its result in a bubble for a while.
|
|
309
|
+
var result = member.busy ? null : memory && memory.busy ? {value: staffResult(floor, member.activity), until: frame + STAFF_FADE} : memory ? memory.result : null;
|
|
310
|
+
staffMemory[member.key] = {busy: member.busy, result: result};
|
|
311
|
+
seen[member.key] = true;
|
|
312
|
+
moveTo(member.key, member.slot, member.target, false);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
Object.keys(people).forEach(function(key){ if (!seen[key]) delete people[key]; });
|
|
316
|
+
Object.keys(staffMemory).forEach(function(key){ if (!seen[key]) delete staffMemory[key]; });
|
|
317
|
+
}
|
|
318
|
+
function stepAlong(state){
|
|
319
|
+
if (!state.path.length) return;
|
|
320
|
+
var next = state.path.shift(), dx = next.x - state.x, dy = next.y - state.y;
|
|
321
|
+
state.dir = Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? "left" : "right") : (dy < 0 ? "up" : "down");
|
|
322
|
+
state.x = next.x; state.y = next.y; state.step++;
|
|
323
|
+
}
|
|
324
|
+
/** People cover two grid cells a frame, the viewer three. */
|
|
325
|
+
function advance(){ Object.keys(people).forEach(function(key){ stepAlong(people[key]); stepAlong(people[key]); }); stepAlong(viewer); stepAlong(viewer); stepAlong(viewer); }
|
|
326
|
+
function walkPose(state){ return state.dir + ["1", "0", "2", "0"][(state.step >> 3) % 4]; }
|
|
327
|
+
/** Everyone drawn in a room: sprite top-left, draw depth, pose and feet. */
|
|
328
|
+
function figures(floor){
|
|
329
|
+
var out = [];
|
|
330
|
+
floor.actors.concat(staffOf(floor)).forEach(function(actor){
|
|
331
|
+
var state = people[actor.key];
|
|
332
|
+
if (!state || state.hidden) return;
|
|
333
|
+
var walking = state.path.length > 0, slot = state.slot;
|
|
334
|
+
if (walking) out.push({actor: actor, x: state.x - 17, y: state.y - 49, z: state.y, pose: walkPose(state), walking: true, feet: {x: state.x, y: state.y}});
|
|
335
|
+
else out.push({actor: actor, x: slot.x, y: slot.y, z: slot.z, pose: actor.questionCount > 0 ? "hand" : slot.pose, walking: false, feet: {x: state.x, y: state.y}});
|
|
336
|
+
});
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
/** A door opens while someone walks through it: closed, ajar, open. */
|
|
340
|
+
function doorFrame(door, walkers){
|
|
341
|
+
var cx = door.gap.x + door.gap.w / 2, cy = door.gap.y + door.gap.h / 2, near = Infinity;
|
|
342
|
+
walkers.forEach(function(p){ near = Math.min(near, Math.abs(p.x - cx) + Math.abs(p.y - cy)); });
|
|
343
|
+
return near < 20 ? 2 : near < 36 ? 1 : 0;
|
|
344
|
+
}
|
|
345
|
+
function noteName(status){ return status === "PASS" ? "notePass" : status === "FAIL" ? "noteFail" : status === "UNKNOWN" ? "noteUnknown" : "notePending"; }
|
|
346
|
+
/** The planning whiteboard carries one sticky note per acceptance criterion. */
|
|
347
|
+
function drawNotes(floor, g){
|
|
348
|
+
var criteria = floor.criteria || [], shown = criteria.length > BOARD_SLOTS ? criteria.slice(0, BOARD_SLOTS - 1) : criteria;
|
|
349
|
+
shown.forEach(function(criterion, index){ var p = notePosition(index); g.drawImage(spriteImage(noteName(criterion.status)), LAYOUT.board.x + p.x, LAYOUT.board.y + p.y); });
|
|
350
|
+
if (criteria.length > BOARD_SLOTS) { var last = notePosition(BOARD_SLOTS - 1); g.drawImage(spriteImage("notePlus"), LAYOUT.board.x + last.x, LAYOUT.board.y + last.y); }
|
|
351
|
+
}
|
|
352
|
+
function staffBubble(actor){ var memory = staffMemory[actor.key], result = memory && memory.result; return result && result.value && frame < result.until ? result : null; }
|
|
353
|
+
function iconFor(floor, actor){
|
|
354
|
+
if (actor.staff) { var shown = staffBubble(actor); return actor.busy ? (reducedMotion || (frame >> 5) % 2 === 0 ? "bubbleBusy" : null) : shown ? (shown.value === "pass" ? "bubbleDone" : "bubbleFail") : null; }
|
|
355
|
+
if (actor.questionCount > 0) return "bubbleAlert";
|
|
356
|
+
if (floor.completedAt || actor.status === "complete") return "bubbleDone";
|
|
357
|
+
if (actor.phase === "implementing" && (reducedMotion || (frame >> 5) % 2 === 0)) return "bubbleBusy";
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
/** Draws one room at 1 art pixel per pixel; returns the people drawn. */
|
|
361
|
+
function renderRoom(floor, g){
|
|
362
|
+
g.imageSmoothingEnabled = false;
|
|
363
|
+
g.drawImage(roomBase(floor), 0, 0);
|
|
364
|
+
var figs = figures(floor), here = mode === "room" && activeRoom() === floor, walkers = figs.filter(function(f){ return f.walking; }).map(function(f){ return f.feet; }), list = [];
|
|
365
|
+
if (here) walkers.push({x: viewer.x, y: viewer.y});
|
|
366
|
+
LAYOUT.items.forEach(function(item){ list.push({z: item.z, x: item.x, y: item.y, image: spriteImage(variantName(item.sprite, item.variant, floor)), board: item.sprite === "board"}); });
|
|
367
|
+
LAYOUT.doors.forEach(function(door){
|
|
368
|
+
var f = doorFrame(door, walkers); doorFrames[floor.key + "|" + door.id] = f;
|
|
369
|
+
list.push({z: door.kind === "side" ? door.y + 34 : LAYOUT.walls.middle.y + LAYOUT.walls.middle.h, x: door.x, y: door.y, image: spriteImage((door.kind === "side" ? "sideDoor" : "frontDoor") + f)});
|
|
370
|
+
});
|
|
371
|
+
figs.forEach(function(f){ list.push({z: f.z, x: f.x, y: f.y, image: personImage(f.actor.id, f.actor.kind, f.pose)}); });
|
|
372
|
+
if (here) list.push({z: viewer.y, x: viewer.x - 17, y: viewer.y - 49, image: personImage("viewer", "viewer", viewer.path.length ? walkPose(viewer) : viewer.dir + "0")});
|
|
373
|
+
list.sort(function(a, b){ return a.z - b.z; }).forEach(function(entry){ g.drawImage(entry.image, entry.x, entry.y); if (entry.board) drawNotes(floor, g); });
|
|
374
|
+
figs.forEach(function(f){
|
|
375
|
+
var icon = iconFor(floor, f.actor), shown = f.actor.staff && staffBubble(f.actor), left = shown ? shown.until - frame : Infinity;
|
|
376
|
+
if (!icon) return;
|
|
377
|
+
// Result bubbles fade over their last second, unless motion is reduced.
|
|
378
|
+
g.globalAlpha = reducedMotion || left >= 60 ? 1 : left / 60;
|
|
379
|
+
g.drawImage(spriteImage(icon), f.x + 11, f.y - 15 - (icon === "bubbleAlert" && !reducedMotion && (frame >> 4) % 2 ? 1 : 0));
|
|
380
|
+
g.globalAlpha = 1;
|
|
381
|
+
});
|
|
382
|
+
return figs;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---- overview: one section per repository, equal cards at one whole-number scale ----
|
|
386
|
+
function roomRank(floor){
|
|
387
|
+
if (floor.questions.length || floor.actors.some(function(a){ return a.questionCount > 0; })) return 0;
|
|
388
|
+
return !floor.completedAt && floor.status !== "idle" && floor.status !== "complete" ? 1 : 2;
|
|
389
|
+
}
|
|
390
|
+
function overviewGroups(){
|
|
391
|
+
var groups = {};
|
|
392
|
+
visibleRooms().forEach(function(floor){ var repo = floor.repo || ""; (groups[repo] = groups[repo] || []).push(floor); });
|
|
393
|
+
return Object.keys(groups).sort().map(function(repo){
|
|
394
|
+
var rooms = groups[repo].map(function(floor, index){ return {floor: floor, index: index}; })
|
|
395
|
+
.sort(function(a, b){ return roomRank(a.floor) - roomRank(b.floor) || a.index - b.index; }).map(function(entry){ return entry.floor; });
|
|
396
|
+
return {repo: repo, rooms: rooms, questions: rooms.reduce(function(n, f){ return n + f.questions.length; }, 0)};
|
|
397
|
+
});
|
|
398
398
|
}
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
399
|
+
function orderedRooms(){ return overviewGroups().reduce(function(all, group){ return all.concat(group.rooms); }, []); }
|
|
400
|
+
var CARD_GAP = 14, CARD_BORDER = 8;
|
|
401
|
+
/** As many columns as fit at 1x or more; the leftover width is shared so the row reaches the right edge. */
|
|
402
|
+
function overviewScale(){
|
|
403
|
+
var width = ((wrap && wrap.clientWidth) || window.innerWidth) - 32, columns = Math.max(1, Math.floor((width + CARD_GAP) / (ROOM_W + CARD_GAP)));
|
|
404
|
+
return Math.max(1, (width - (columns - 1) * CARD_GAP - columns * CARD_BORDER) / columns / ROOM_W);
|
|
405
|
+
}
|
|
406
|
+
function summaryFor(floor){
|
|
407
|
+
var criteria = floor.criteria || [], passed = criteria.filter(function(c){ return c.status === "PASS"; }).length;
|
|
408
|
+
return phaseLabel(floor.phase) + " · " + (criteria.length ? t("checks") + " " + passed + "/" + criteria.length : statusText(floor.status)) + (floor.questions.length ? " · ! " + floor.questions.length : "");
|
|
409
|
+
}
|
|
410
|
+
var PAGE_SIZE = 4;
|
|
411
|
+
/** How far a folded card may stretch from the room's 9:5 to fill the page. */
|
|
412
|
+
var MAX_STRETCH = 1.18;
|
|
413
|
+
/**
|
|
414
|
+
* Folded list: rooms come in pages of up to four, each page one viewport tall
|
|
415
|
+
* in a scroller that snaps page by page. Each page takes the column count that
|
|
416
|
+
* gives its cards the most area (four rooms form a 2x2 grid, two split the page
|
|
417
|
+
* in halves, one fills it), and cards stretch to fill their cell while their
|
|
418
|
+
* shape stays within MAX_STRETCH of 9:5.
|
|
419
|
+
*/
|
|
420
|
+
function pageHeight(){ return Math.max(160, Math.floor((wrap && wrap.clientHeight) || window.innerHeight - 70)); }
|
|
421
|
+
function pageLayout(count, inset){
|
|
422
|
+
var width = ((wrap && wrap.clientWidth) || window.innerWidth) - 32, height = pageHeight() - 16 - (inset || 0), best = {columns: 1, sx: 0.5, sy: 0.5};
|
|
423
|
+
for (var columns = 1; columns <= Math.max(1, count); columns++) {
|
|
424
|
+
var rows = Math.ceil(Math.max(1, count) / columns);
|
|
425
|
+
var sx = (width - (columns - 1) * CARD_GAP - columns * CARD_BORDER) / columns / ROOM_W, sy = (height - (rows - 1) * CARD_GAP - rows * CARD_BORDER) / rows / ROOM_H;
|
|
426
|
+
if (sx > sy * MAX_STRETCH) sx = sy * MAX_STRETCH; else if (sy > sx * MAX_STRETCH) sy = sx * MAX_STRETCH;
|
|
427
|
+
if (sx * sy > best.sx * best.sy) best = {columns: columns, sx: sx, sy: sy};
|
|
428
|
+
}
|
|
429
|
+
return best;
|
|
430
|
+
}
|
|
431
|
+
function pageCount(){ return Math.max(1, Math.ceil(orderedRooms().length / PAGE_SIZE)); }
|
|
432
|
+
function pageInView(){ return Math.max(0, Math.min(pageCount() - 1, Math.round((overviewBox.scrollTop || 0) / pageHeight()))); }
|
|
433
|
+
function pageRooms(page){ return orderedRooms().slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE); }
|
|
434
|
+
function roomCard(floor, number, sx, sy){
|
|
435
|
+
var card = document.createElement("button"), view = makeCanvas();
|
|
436
|
+
card.type = "button"; card.className = "card"; card.setAttribute("data-key", floor.key); card.setAttribute("aria-label", t("enter") + ": " + floor.title);
|
|
437
|
+
view.style.width = Math.floor(ROOM_W * sx) + "px"; view.style.height = Math.floor(ROOM_H * (sy || sx)) + "px";
|
|
438
|
+
card.appendChild(view);
|
|
439
|
+
var caption = document.createElement("span"); caption.className = "card-caption";
|
|
440
|
+
caption.appendChild(panelText("span", "card-title", (number <= 9 ? number + ". " : "") + floor.title));
|
|
441
|
+
caption.appendChild(panelText("span", "card-meta", summaryFor(floor)));
|
|
442
|
+
card.appendChild(caption);
|
|
443
|
+
card.addEventListener("click", function(){ enterRoom(floor.key); });
|
|
444
|
+
roomCanvases[floor.key] = view;
|
|
445
|
+
return card;
|
|
446
|
+
}
|
|
447
|
+
/** Nobody at work: the office still shows one empty room. */
|
|
448
|
+
var EMPTY_FLOOR = {key: "__empty", title: "", kind: "desk", actors: [], questions: [], criteria: [], phase: "unknown", status: "idle", completedAt: null, repo: null,
|
|
449
|
+
board: {passed: 0, total: 0, verify: "pending", review: "pending", pending: 0, status: "idle", taskId: "unassigned"}};
|
|
450
|
+
function emptyCard(){
|
|
451
|
+
var layout = pageLayout(1, panelCollapsed ? 0 : 24), card = el("div", "card empty-room"), view = makeCanvas();
|
|
452
|
+
view.style.width = Math.floor(ROOM_W * layout.sx) + "px"; view.style.height = Math.floor(ROOM_H * layout.sy) + "px";
|
|
453
|
+
card.appendChild(view);
|
|
454
|
+
var caption = el("span", "card-caption"); caption.appendChild(el("span", "card-title", t("quiet"))); card.appendChild(caption);
|
|
455
|
+
roomCanvases[EMPTY_FLOOR.key] = view;
|
|
456
|
+
// The open overview pads itself; leave that room so a lone card never scrolls.
|
|
457
|
+
var sheet = el("section", "page"); sheet.style.height = (pageHeight() - (panelCollapsed ? 0 : 24)) + "px"; sheet.appendChild(card);
|
|
458
|
+
return sheet;
|
|
459
|
+
}
|
|
460
|
+
function drawCards(){ Object.keys(roomCanvases).forEach(function(key){ var floor = key === EMPTY_FLOOR.key ? EMPTY_FLOOR : roomByKey(key); if (floor) renderRoom(floor, roomCanvases[key].getContext("2d")); }); }
|
|
461
|
+
function renderOverviewPages(){
|
|
462
|
+
var pages = pageCount(), height = pageHeight();
|
|
463
|
+
var signature = JSON.stringify(["pages", lang, height, (wrap && wrap.clientWidth) || window.innerWidth, orderedRooms().map(function(f){ return [f.key, f.title, f.status, f.phase, f.completedAt, f.questions.length, (f.criteria || []).map(function(c){ return c.status; })]; })]);
|
|
464
|
+
if (signature !== overviewSignature) {
|
|
465
|
+
overviewSignature = signature; overviewBox.textContent = ""; roomCanvases = {};
|
|
466
|
+
if (!orderedRooms().length) overviewBox.appendChild(emptyCard());
|
|
467
|
+
for (var page = 0; page < pages && orderedRooms().length; page++) {
|
|
468
|
+
var rooms = pageRooms(page), layout = pageLayout(rooms.length), sheet = document.createElement("section"), grid = document.createElement("div");
|
|
469
|
+
sheet.className = "page"; sheet.setAttribute("data-page", String(page + 1)); sheet.style.height = height + "px";
|
|
470
|
+
grid.className = "cards page-grid"; grid.style.gridTemplateColumns = "repeat(" + layout.columns + ", max-content)";
|
|
471
|
+
rooms.forEach(function(floor, index){ grid.appendChild(roomCard(floor, index + 1, layout.sx, layout.sy)); });
|
|
472
|
+
sheet.appendChild(grid); overviewBox.appendChild(sheet);
|
|
410
473
|
}
|
|
411
474
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
475
|
+
drawCards();
|
|
476
|
+
}
|
|
477
|
+
function renderOverview(){
|
|
478
|
+
overviewBox.className = panelCollapsed ? "paged" : "";
|
|
479
|
+
if (panelCollapsed) return renderOverviewPages();
|
|
480
|
+
var groups = overviewGroups(), size = overviewScale();
|
|
481
|
+
var signature = JSON.stringify([lang, size, groups.map(function(group){ return [group.repo, group.questions, group.rooms.map(function(f){ return [f.key, f.title, f.status, f.phase, f.completedAt, f.questions.length, (f.criteria || []).map(function(c){ return c.status; })]; })]; })]);
|
|
482
|
+
if (signature !== overviewSignature) {
|
|
483
|
+
overviewSignature = signature; overviewBox.textContent = ""; roomCanvases = {};
|
|
484
|
+
if (!groups.length) overviewBox.appendChild(emptyCard());
|
|
485
|
+
var number = 0;
|
|
486
|
+
groups.forEach(function(group){
|
|
487
|
+
var section = document.createElement("section"); section.className = "repo-section"; section.setAttribute("data-repo", group.repo);
|
|
488
|
+
var head = document.createElement("h2"); head.className = "repo-head";
|
|
489
|
+
head.appendChild(panelText("span", "repo-name", group.repo || t("overview")));
|
|
490
|
+
head.appendChild(panelText("span", "repo-alert", group.questions ? "! " + group.questions : ""));
|
|
491
|
+
section.appendChild(head);
|
|
492
|
+
var cards = document.createElement("div"); cards.className = "cards";
|
|
493
|
+
group.rooms.forEach(function(floor){ number++; cards.appendChild(roomCard(floor, number, size)); });
|
|
494
|
+
section.appendChild(cards); overviewBox.appendChild(section);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
drawCards();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---- room view: the pixel-exact room layer stretched to fill the space, text in HTML over it ----
|
|
501
|
+
/** The room view fills the space, stretching at most MAX_STRETCH from 9:5. */
|
|
502
|
+
function roomScales(){
|
|
503
|
+
var width = ((wrap && wrap.clientWidth) || window.innerWidth) - 32, height = ((wrap && wrap.clientHeight) || window.innerHeight) - ((hud && hud.offsetHeight) || 44) - 40;
|
|
504
|
+
var sx = Math.max(0.5, width / ROOM_W), sy = Math.max(0.5, height / ROOM_H);
|
|
505
|
+
if (sx > sy * MAX_STRETCH) sx = sy * MAX_STRETCH; else if (sy > sx * MAX_STRETCH) sy = sx * MAX_STRETCH;
|
|
506
|
+
return {x: sx, y: sy};
|
|
507
|
+
}
|
|
508
|
+
function renderRoomView(){
|
|
509
|
+
var floor = activeRoom(); if (!floor) return;
|
|
510
|
+
var scales = roomScales(); scale = scales.x; scaleY = scales.y;
|
|
511
|
+
// The CSS box fills the space; the backing store follows the device pixels so nearest-neighbour stays sharp.
|
|
512
|
+
var cssW = Math.round(ROOM_W * scale), cssH = Math.round(ROOM_H * scaleY), dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
|
|
513
|
+
var backW = Math.round(cssW * dpr), backH = Math.round(cssH * dpr);
|
|
514
|
+
if (canvas.width !== backW) canvas.width = backW;
|
|
515
|
+
if (canvas.height !== backH) canvas.height = backH;
|
|
516
|
+
canvas.style.width = cssW + "px"; canvas.style.height = cssH + "px";
|
|
517
|
+
labels.style.width = canvas.style.width; labels.style.height = canvas.style.height;
|
|
518
|
+
if (!stageCanvas) stageCanvas = makeCanvas();
|
|
519
|
+
lastFigures = renderRoom(floor, stageCanvas.getContext("2d"));
|
|
520
|
+
ctx.imageSmoothingEnabled = false;
|
|
521
|
+
ctx.drawImage(stageCanvas, 0, 0, backW, backH);
|
|
522
|
+
renderHud(floor); renderLabels(floor, lastFigures);
|
|
523
|
+
}
|
|
524
|
+
function renderHud(floor){
|
|
525
|
+
var criteria = floor.criteria || [], passed = criteria.filter(function(c){ return c.status === "PASS"; }).length, questions = floor.questions.length;
|
|
526
|
+
var signature = JSON.stringify([lang, floor.key, floor.title, passed, criteria.length, floor.board.verify, floor.board.review, questions]);
|
|
527
|
+
if (signature === hudSignature) return;
|
|
528
|
+
hudSignature = signature; hud.textContent = "";
|
|
529
|
+
hud.appendChild(panelText("strong", "hud-title", floor.title));
|
|
530
|
+
var bar = document.createElement("span"), done = document.createElement("i");
|
|
531
|
+
bar.className = "hud-bar"; bar.setAttribute("role", "img"); bar.setAttribute("aria-label", t("checks") + " " + passed + "/" + criteria.length);
|
|
532
|
+
done.style.width = (criteria.length ? Math.round(passed * 100 / criteria.length) : 0) + "%"; bar.appendChild(done); hud.appendChild(bar);
|
|
533
|
+
hud.appendChild(panelText("span", "hud-count", passed + "/" + criteria.length));
|
|
534
|
+
var tone = function(value){ return value === "FAIL" ? " bad" : value === "PASS" ? " good" : ""; };
|
|
535
|
+
hud.appendChild(panelText("span", "badge" + tone(floor.board.verify), t("verify") + " " + outcomeText(floor.board.verify)));
|
|
536
|
+
hud.appendChild(panelText("span", "badge" + tone(floor.board.review), t("review") + " " + outcomeText(floor.board.review)));
|
|
537
|
+
if (questions) {
|
|
538
|
+
var ask = document.createElement("button"); ask.type = "button"; ask.className = "badge alert"; ask.textContent = "! " + questions + " " + t("pending");
|
|
539
|
+
ask.addEventListener("click", function(){ openQuestionList(floor); }); hud.appendChild(ask);
|
|
446
540
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
541
|
+
}
|
|
542
|
+
function label(key, className, text, x, y){
|
|
543
|
+
var node = labelNodes[key];
|
|
544
|
+
if (!node) { node = document.createElement("span"); labelNodes[key] = node; labels.appendChild(node); }
|
|
545
|
+
node.className = className; if (node.textContent !== text) node.textContent = text;
|
|
546
|
+
node.style.left = x * scale + "px"; node.style.top = y * scaleY + "px"; node.hidden = false;
|
|
547
|
+
return node;
|
|
548
|
+
}
|
|
549
|
+
function resetLabels(){ labels.textContent = ""; labelNodes = {}; hudSignature = ""; }
|
|
550
|
+
function nearestActor(figs){
|
|
551
|
+
var best = null, distance = 30;
|
|
552
|
+
figs.forEach(function(f){ var d = Math.abs(f.feet.x - viewer.x) + Math.abs(f.feet.y - viewer.y); if (d < distance) { distance = d; best = f; } });
|
|
553
|
+
return best;
|
|
554
|
+
}
|
|
555
|
+
function renderLabels(floor, figs){
|
|
556
|
+
Object.keys(labelNodes).forEach(function(key){ labelNodes[key].hidden = true; });
|
|
557
|
+
LAYOUT.rooms.forEach(function(room){ label("zone:" + room.id, "zone-label", phaseLabel(room.id), room.x + 4, room.y + 2); });
|
|
558
|
+
var counts = placements(floor).counts;
|
|
559
|
+
LAYOUT.rooms.forEach(function(room){
|
|
560
|
+
var extra = (counts[room.id] || 0) - room.slots.length;
|
|
561
|
+
if (extra > 0) { var last = room.slots[room.slots.length - 1]; label("more:" + room.id, "overflow", "+" + extra, last.x + 30, last.y); }
|
|
562
|
+
});
|
|
563
|
+
figs.forEach(function(f){ label("name:" + f.actor.key, "nameplate" + (f.actor.questionCount ? " alert" : ""), shortName(f.actor.id, 14), f.x + 17, f.y + 50); });
|
|
564
|
+
label("name:viewer", "nameplate viewer", t("viewer"), viewer.x, viewer.y + 1);
|
|
565
|
+
var near = nearestActor(figs);
|
|
566
|
+
if (near) label("speech", "speech", shortName(near.actor.id, 14) + ": " + localizeText(near.actor.narration), near.x + 17, near.y - 6);
|
|
567
|
+
}
|
|
568
|
+
function render(){ if (!model) return; if (mode === "room") renderRoomView(); else renderOverview(); }
|
|
569
|
+
function tick(){ frame++; if (!reducedMotion) advance(); render(); }
|
|
570
|
+
function attentionFor(actor){
|
|
571
|
+
if(actor.questionCount)return t('waiting')+' ('+actor.questionCount+')';
|
|
572
|
+
if(actor.progress&&actor.progress.verify==='FAIL')return t('verifyFailed');
|
|
573
|
+
if(actor.progress&&actor.progress.review==='FAIL')return t('reviewFailed');
|
|
574
|
+
return actor.status==='blocked'?t('blockedUnknown'):'';
|
|
575
|
+
}
|
|
576
|
+
function completionText(floor,actor){
|
|
577
|
+
if(actor.status==='complete')return statusText(actor.status);
|
|
578
|
+
if(floor.completedAt)return t('sessionEnded');
|
|
579
|
+
if(actor.taskId==='unassigned'&&!actor.progress)return t('noTask');
|
|
580
|
+
return statusText(actor.status)+' · '+t('unfinished');
|
|
581
|
+
}
|
|
582
|
+
function renderWorkPanel(){
|
|
583
|
+
if(!model||!workList)return;
|
|
584
|
+
var rooms=mode==='room'&&activeRoom()?[activeRoom()]:visibleRooms();
|
|
585
|
+
var signature=JSON.stringify([lang,offline,mode,rooms.map(function(f){return [f.key,f.title,f.status,f.phase,f.completedAt,f.actors,f.questions,f.criteria];})]);
|
|
586
|
+
if(signature===workSignature)return;workSignature=signature;
|
|
587
|
+
if(mode==='room'&&activeRoom()){renderRoomPanel(activeRoom());return;}
|
|
588
|
+
var focusKey=document.activeElement&&document.activeElement.getAttribute?document.activeElement.getAttribute('data-work-key'):null,restoreFocus=null,entries=[];
|
|
589
|
+
rooms.forEach(function(floor){floor.actors.forEach(function(actor){entries.push({floor:floor,actor:actor,attention:attentionFor(actor)});});});
|
|
590
|
+
entries.sort(function(a,b){return Number(!!b.attention)-Number(!!a.attention);});
|
|
591
|
+
workHeading.textContent=t('workList')+' · '+entries.length;
|
|
592
|
+
workSummary.textContent=offline?t('stale'):t('attention')+' '+entries.filter(function(entry){return !!entry.attention;}).length+' · '+t('detailsAction');
|
|
593
|
+
workList.textContent='';
|
|
594
|
+
if(!entries.length){var empty=document.createElement('p');empty.textContent=t('quiet');workList.appendChild(empty);}
|
|
595
|
+
entries.forEach(function(entry){
|
|
596
|
+
var floor=entry.floor,actor=entry.actor,card=document.createElement('article'),button=document.createElement('button');
|
|
597
|
+
card.className='work-card'+(entry.attention?' attention':'');
|
|
598
|
+
button.type='button';button.className='work-name';button.textContent=actor.id+' ↗';button.title=t('detailsAction');
|
|
599
|
+
button.setAttribute('aria-label',actor.id+' · '+t('detailsAction'));button.setAttribute('data-work-key',actor.key+':details');
|
|
600
|
+
button.addEventListener('click',function(){openActorDetail(floor,actor);});card.appendChild(button);
|
|
601
|
+
if(focusKey===actor.key+':details')restoreFocus=button;
|
|
602
|
+
var meta=document.createElement('p');meta.className='work-meta';meta.textContent=(floor.repo?floor.repo+' · ':'')+roleText(actor.kind)+' · '+phaseLabel(actor.phase)+' · '+completionText(floor,actor);card.appendChild(meta);
|
|
603
|
+
var work=document.createElement('p');work.textContent=localizeText(actor.narration);card.appendChild(work);
|
|
604
|
+
var proof=document.createElement('p');proof.className='work-proof';proof.textContent=actor.taskId==='unassigned'&&!actor.progress?t('noTaskHint'):progressText(actor.progress);card.appendChild(proof);
|
|
605
|
+
if(entry.attention){var reason=document.createElement('p');reason.className='work-reason';reason.textContent='! '+entry.attention;card.appendChild(reason);}
|
|
606
|
+
if(actor.questionCount){
|
|
607
|
+
var question=document.createElement('p');question.textContent=floor.questions[0]?floor.questions[0].prompt:t('unknown');card.appendChild(question);
|
|
608
|
+
var answer=document.createElement('button');answer.type='button';answer.className='work-action';answer.textContent=t('openQuestion')+' →';answer.setAttribute('data-work-key',actor.key+':questions');answer.addEventListener('click',function(){openQuestionList(floor);});card.appendChild(answer);
|
|
609
|
+
if(focusKey===actor.key+':questions')restoreFocus=answer;
|
|
459
610
|
}
|
|
611
|
+
workList.appendChild(card);
|
|
460
612
|
});
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
613
|
+
if(focusKey&&statusBox.hidden)(restoreFocus||canvas).focus();
|
|
614
|
+
}
|
|
615
|
+
function criterionStatusText(status){return status==='PASS'?t('critPass'):status==='FAIL'?t('critFail'):status==='UNKNOWN'?t('critUnknown'):t('critPending');}
|
|
616
|
+
function panelText(tag,className,text){var node=document.createElement(tag);if(className)node.className=className;node.textContent=text;return node;}
|
|
617
|
+
// Room mode: the right column lists this room's acceptance criteria, questions and commands. Task text is data, so textContent only.
|
|
618
|
+
function renderRoomPanel(floor){
|
|
619
|
+
var criteria=floor.criteria||[],passed=criteria.filter(function(c){return c.status==='PASS';}).length;
|
|
620
|
+
workHeading.textContent=t('checks')+' · '+passed+'/'+criteria.length;
|
|
621
|
+
workSummary.textContent=offline?t('stale'):floor.title;
|
|
622
|
+
workList.textContent='';
|
|
623
|
+
if(!criteria.length)workList.appendChild(panelText('p','',t('noCriteria')));
|
|
624
|
+
criteria.forEach(function(c){
|
|
625
|
+
var card=document.createElement('article');card.className='work-card criterion '+(c.status?c.status.toLowerCase():'pending');card.setAttribute('data-criterion',c.id);
|
|
626
|
+
card.appendChild(panelText('p','work-name',c.id+' · '+criterionStatusText(c.status)));
|
|
627
|
+
card.appendChild(panelText('p','',c.description));
|
|
628
|
+
var meta=[];if(c.status&&c.status!=='PASS'){if(c.failureClass)meta.push(c.failureClass);if(c.exitCode!==null&&c.exitCode!==undefined)meta.push('exit '+c.exitCode);}
|
|
629
|
+
if(c.finishedAt)meta.push(c.finishedAt.replace('T',' ').slice(0,16));
|
|
630
|
+
if(meta.length)card.appendChild(panelText('p','work-meta',meta.join(' · ')));
|
|
631
|
+
workList.appendChild(card);
|
|
467
632
|
});
|
|
468
|
-
floor.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
var pendingRow=pendingBoardGeometry(canvas.width,canvas.height);
|
|
481
|
-
if(b.pending)fillText('! '+t('pending')+' '+b.pending,pendingRow.x+2*unit,pendingRow.y+3*unit,8,PALETTE[13]);
|
|
482
|
-
var oldS=S;S=Math.max(1,Math.round(unit));var cx=52*rx,cy=ry;draw('clock',cx,cy);
|
|
483
|
-
ctx.strokeStyle=PALETTE[13];ctx.lineWidth=2*S;ctx.beginPath();ctx.moveTo(cx+8*S,cy+7*S);var angle=-Math.PI/2+2*Math.PI*floor.clock;ctx.lineTo(cx+8*S+Math.cos(angle)*5*S,cy+7*S+Math.sin(angle)*5*S);ctx.stroke();S=oldS;
|
|
484
|
-
U=savedU;
|
|
485
|
-
ctx.strokeStyle=PALETTE[9];ctx.lineWidth=3*unit;ctx.strokeRect(unit,unit,canvas.width-2*unit,canvas.height-2*unit);
|
|
486
|
-
}
|
|
487
|
-
function drawActor(floor, actor, surface){
|
|
488
|
-
var tx=actor.x*T,ty=actor.y*T,pos=positions[actor.key]||{x:tx,y:ty};
|
|
489
|
-
advanceAvatar(pos,tx,ty,T/5);positions[actor.key]=pos;
|
|
490
|
-
surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
|
|
491
|
-
var box=actorBox(actor,surface),oldS=S,oldU=U;S=box.scale;U=box.unit;
|
|
492
|
-
var px=box.x,py=box.y;
|
|
493
|
-
drawAvatar(actor.id,pos,px,py,S,false);
|
|
494
|
-
if(!pos.walking&&!reducedMotion&&Math.floor(frame/30)%2===0){ctx.fillStyle=PALETTE[15];ctx.fillRect(px+12*S,py+30*S,8*S,4*S);}
|
|
495
|
-
var text=short(actor.label,10)+' · '+roleText(actor.kind)+' · '+short(statusText(actor.status),8),font=9*U;
|
|
496
|
-
ctx.font='bold '+font+'px monospace';var labelWidth=Math.min(surface.width-8*U,(ctx.measureText?ctx.measureText(text).width:text.length*font*.62)+8*U),labelX=Math.max(surface.x+4*U,Math.min(surface.x+surface.width-labelWidth-4*U,px-3*U)),labelY=Math.min(surface.y+surface.height-16*U,py+49*S);
|
|
497
|
-
ctx.fillStyle=PALETTE[15];ctx.fillRect(labelX,labelY,labelWidth,14*U);ctx.strokeStyle=PALETTE[8];ctx.lineWidth=U;ctx.strokeRect(labelX,labelY,labelWidth,14*U);fillText(text,labelX+3*U,labelY+2*U,9,actor.alert?PALETTE[13]:PALETTE[0]);
|
|
498
|
-
var alert=alertGeometry(actor,surface);
|
|
499
|
-
if(actor.alert)draw('alert',alert.x,alert.y);
|
|
500
|
-
S=oldS;U=oldU;
|
|
501
|
-
}
|
|
502
|
-
function actorBox(actor,surface){
|
|
503
|
-
var pos=positions[actor.key]||{x:actor.x*T,y:actor.y*T},scale=avatarScale(surface),unit=Math.max(dpr,Math.min(U,scale));
|
|
504
|
-
return {scale:scale,unit:unit,x:Math.round(Math.max(surface.x,Math.min(surface.x+surface.width-32*scale,surface.x+pos.x/(model.roomCols*T)*surface.width))),y:Math.round(Math.max(surface.y,Math.min(surface.y+surface.height-48*scale-16*unit,surface.y+pos.y/(model.roomRows*T)*surface.height)))};
|
|
505
|
-
}
|
|
506
|
-
function alertGeometry(actor,surface){var box=actorBox(actor,surface);return {x:box.x+14*box.scale,y:box.y-9*box.scale,width:4*box.scale,height:8*box.scale,scale:box.scale};}
|
|
507
|
-
function fitCanvas(){
|
|
508
|
-
var wrapW = wrap && wrap.clientWidth ? wrap.clientWidth : window.innerWidth;
|
|
509
|
-
var wrapH = wrap && wrap.clientHeight ? wrap.clientHeight : window.innerHeight - 54;
|
|
510
|
-
var reserved = (dialogueBox && dialogueBox.offsetHeight || 36) + (roomNav && roomNav.offsetHeight || 30) + 24;
|
|
511
|
-
var maxW = Math.max(220, wrapW - 18), maxH = Math.max(160, wrapH - reserved);
|
|
512
|
-
var ratio = Math.min(maxW / canvas.width, maxH / canvas.height);
|
|
513
|
-
canvas.style.width = Math.max(1, Math.floor(canvas.width * ratio)) + "px"; canvas.style.height = Math.max(1, Math.floor(canvas.height * ratio)) + "px";
|
|
514
|
-
}
|
|
515
|
-
function chooseRenderScale(){
|
|
516
|
-
if(!model)return;
|
|
517
|
-
var wrapW=wrap&&wrap.clientWidth?wrap.clientWidth:window.innerWidth,wrapH=wrap&&wrap.clientHeight?wrap.clientHeight:window.innerHeight-54;
|
|
518
|
-
var reserved=(dialogueBox&&dialogueBox.offsetHeight||36)+(roomNav&&roomNav.offsetHeight||30)+24;
|
|
519
|
-
var maxW=Math.max(220,wrapW-18),maxH=Math.max(160,wrapH-reserved),nextDpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)),oldT=T;
|
|
520
|
-
model.cols=model.rows*maxW/maxH;
|
|
521
|
-
T=Math.max(4,maxH/model.rows*nextDpr);S=T/4;U=T/8;dpr=nextDpr;
|
|
522
|
-
if(oldT!==T){Object.keys(positions).forEach(function(key){positions[key].x*=T/oldT;positions[key].y*=T/oldT;});roomCanvases={};}
|
|
523
|
-
}
|
|
524
|
-
function overviewLayout(rooms){
|
|
525
|
-
var count=rooms.length,gap=.6,result=[];if(!count)return result;
|
|
526
|
-
if(count===1)return [{x:gap,y:gap,width:model.cols-6-gap,height:model.rows-gap*2}];
|
|
527
|
-
var hallW=6,left=(model.cols-hallW)/2,right=left,teamIndex=rooms.findIndex(function(f){return f.kind==='run';});
|
|
528
|
-
if(teamIndex>=0&&count<=6){
|
|
529
|
-
left=(model.cols-hallW)*.58;right=model.cols-hallW-left;
|
|
530
|
-
result[teamIndex]={x:gap,y:gap,width:left-gap,height:model.rows-gap*2};
|
|
531
|
-
var others=rooms.filter(function(_,i){return i!==teamIndex;}),height=(model.rows-gap*(others.length+1))/Math.max(1,others.length);
|
|
532
|
-
others.forEach(function(f,i){result[rooms.indexOf(f)]={x:left+hallW,y:gap+i*(height+gap),width:right-gap,height:height};});
|
|
533
|
-
return result;
|
|
633
|
+
if(floor.questions.length){
|
|
634
|
+
workList.appendChild(panelText('h3','work-section',t('questions')+' · '+floor.questions.length));
|
|
635
|
+
floor.questions.forEach(function(question){
|
|
636
|
+
var card=document.createElement('article');card.className='work-card attention';card.appendChild(panelText('p','',question.prompt));
|
|
637
|
+
var answer=document.createElement('button');answer.type='button';answer.className='work-action';answer.textContent=t('openQuestion')+' →';answer.addEventListener('click',function(){openQuestionList(floor);});card.appendChild(answer);
|
|
638
|
+
workList.appendChild(card);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
var source=sourceFor(floor);
|
|
642
|
+
if(source&&source.commands&&source.commands.length){
|
|
643
|
+
workList.appendChild(panelText('h3','work-section',t('commands')));
|
|
644
|
+
source.commands.forEach(function(command){workList.appendChild(panelText('code','work-command',command));});
|
|
534
645
|
}
|
|
535
|
-
var leftCount=Math.ceil(count/2),rightCount=Math.floor(count/2);
|
|
536
|
-
rooms.forEach(function(f,i){var side=i%2,n=side?rightCount:leftCount,row=Math.floor(i/2),height=(model.rows-gap*(n+1))/n;result[i]={x:side?left+hallW:gap,y:gap+row*(height+gap),width:(side?right:left)-gap,height:height};});
|
|
537
|
-
return result;
|
|
538
|
-
}
|
|
539
|
-
function roomBox(floor){var rooms=visibleRooms(),index=rooms.indexOf(floor);return overviewLayout(rooms)[index]||floor.overview;}
|
|
540
|
-
function hallwayBounds(){
|
|
541
|
-
var rooms=visibleRooms(),layout=overviewLayout(rooms),left=layout.find(function(box){return box.x<model.cols/2-3;});
|
|
542
|
-
return {x:left?left.x+left.width:model.cols/2-3,width:6,y:.6,height:model.rows-1.2};
|
|
543
|
-
}
|
|
544
|
-
function ensureSupervisor(){
|
|
545
|
-
if (supervisorReady || !model) return;
|
|
546
|
-
var hall = hallwayBounds(); supervisor.x = supervisor.targetX = hall.x + hall.width / 2; supervisor.y = supervisor.targetY = model.rows - 5; supervisorReady = true;
|
|
547
|
-
}
|
|
548
|
-
function roomEntrances(){
|
|
549
|
-
var hall=hallwayBounds();return visibleRooms().map(function(floor){var box=roomBox(floor),left=box.x<hall.x;return {floor:floor,x:left?hall.x+.35:hall.x+hall.width-.35,y:box.y+box.height/2,height:box.height};});
|
|
550
|
-
}
|
|
551
|
-
function supervisorBounds(){var hall=hallwayBounds();return {minX:hall.x+.2,maxX:hall.x+hall.width-.2,minY:1,maxY:model.rows-3};}
|
|
552
|
-
function drawHallway(){
|
|
553
|
-
var hall = hallwayBounds(), x = hall.x * T, y = hall.y * T, w = hall.width * T, h = hall.height * T;
|
|
554
|
-
ctx.fillStyle = "rgba(183,199,163,.55)"; ctx.fillRect(x, y, w, h);
|
|
555
|
-
ctx.strokeStyle = PALETTE[3]; ctx.lineWidth = Math.max(1, 2 * U); ctx.strokeRect(x, y, w, h);
|
|
556
|
-
for (var line = y + 14 * U; line < y + h; line += 28 * U) { ctx.strokeStyle = "rgba(138,80,51,.28)"; ctx.beginPath(); ctx.moveTo(x + 3 * U, line); ctx.lineTo(x + w - 3 * U, line); ctx.stroke(); }
|
|
557
|
-
var oldS=S;S=T/8;draw('plant',x+T,y+T,PALETTE[2]);draw('plant',x+T,y+h-4*T,PALETTE[2]);S=oldS;
|
|
558
|
-
roomEntrances().forEach(function(entry){ var ex = entry.x * T, ey = entry.y * T; ctx.fillStyle = PALETTE[6]; ctx.fillRect(ex - 5 * U, ey - 14 * U, 10 * U, 28 * U); ctx.strokeStyle = PALETTE[4]; ctx.strokeRect(ex - 5 * U, ey - 14 * U, 10 * U, 28 * U); });
|
|
559
|
-
}
|
|
560
|
-
function drawSupervisor(){
|
|
561
|
-
ensureSupervisor(); if (!model) return;
|
|
562
|
-
advanceAvatar(supervisor,supervisor.targetX,supervisor.targetY,.7);
|
|
563
|
-
var moving=supervisor.walking,scale=Math.max(1,...visibleRooms().map(function(floor){var b=roomBox(floor);return avatarScale({width:b.width*T,height:b.height*T});}));
|
|
564
|
-
var px=supervisor.x*T-16*scale,py=Math.min(canvas.height-48*scale,supervisor.y*T);drawAvatar('viewer',supervisor,px,py,scale,true);
|
|
565
|
-
drawViewerBadge(supervisor.x*T,py,7);
|
|
566
|
-
var entry=roomEntrances().sort(function(a,b){return Math.hypot(a.x-supervisor.x,a.y-supervisor.y)-Math.hypot(b.x-supervisor.x,b.y-supervisor.y);})[0]; if(entry&&(Math.abs(entry.x-supervisor.x)>1.25||Math.abs(entry.y-supervisor.y)>Math.min(1.6,entry.height*.35)))entry=null;
|
|
567
|
-
if (!moving && mode === "overview" && entry) { selectedKey = entry.floor.key; hallwayReturn = {x:supervisor.x, y:supervisor.y}; enterRoomFromHallway(entry.floor); }
|
|
568
|
-
}
|
|
569
|
-
function enterRoomFromHallway(floor){
|
|
570
|
-
if (!floor) return;
|
|
571
|
-
detailTarget=null;detailPage=0;
|
|
572
|
-
roomSupervisor.x = roomSupervisor.targetX = floor.kind === "run" ? model.roomCols - 7 : 6;
|
|
573
|
-
roomSupervisor.y = roomSupervisor.targetY = Math.max(8, Math.min(model.roomRows - 6, Math.round(model.roomRows / 2)));
|
|
574
|
-
mode = "room"; lineIndex = 0; typed = 0; updateHeader(); canvas.focus();
|
|
575
|
-
}
|
|
576
|
-
function drawRoomSupervisor(){
|
|
577
|
-
if (!model) return;
|
|
578
|
-
advanceAvatar(roomSupervisor,roomSupervisor.targetX,roomSupervisor.targetY,.7);
|
|
579
|
-
var scale=avatarScale({width:canvas.width,height:canvas.height});
|
|
580
|
-
var px=roomSupervisor.x/model.roomCols*canvas.width-16*scale,py=Math.min(canvas.height-48*scale,roomSupervisor.y/model.roomRows*canvas.height);drawAvatar('viewer',roomSupervisor,px,py,scale,true);
|
|
581
|
-
drawViewerBadge(px+16*scale,py,8);
|
|
582
|
-
}
|
|
583
|
-
function drawViewerBadge(center,top,size){
|
|
584
|
-
var text=t("viewer"),font=Math.max(5,Math.round(size*U)),pad=Math.max(2,3*U);ctx.font="bold "+font+"px monospace";
|
|
585
|
-
var width=ctx.measureText(text).width+pad*2,height=Math.max(13*U,font+pad*2),x=Math.max(pad,Math.min(canvas.width-width-pad,center-width/2)),y=Math.max(pad,top-height-3*U);
|
|
586
|
-
ctx.fillStyle=PALETTE[15];ctx.fillRect(x,y,width,height);ctx.strokeStyle=PALETTE[1];ctx.strokeRect(x,y,width,height);fillText(text,x+pad,y+pad,size,PALETTE[1]);
|
|
587
|
-
}
|
|
588
|
-
function moveSupervisor(dx, dy){
|
|
589
|
-
if (!model || !statusBox.hidden) return;
|
|
590
|
-
var state=mode==='room'?roomSupervisor:supervisor;state.direction=dx?(dx<0?'left':'right'):(dy<0?'up':'down');
|
|
591
|
-
var step = mode === "room" ? 1 : Math.min(1.25,(model.rows-2)/Math.max(1,Math.ceil(visibleRooms().length/2))/3);
|
|
592
|
-
if (mode === "room") { roomSupervisor.targetX = Math.max(2, Math.min(model.roomCols - 5, roomSupervisor.targetX + dx * step)); roomSupervisor.targetY = Math.max(7, Math.min(model.roomRows - 7, roomSupervisor.targetY + dy * step)); if (reducedMotion) { roomSupervisor.x = roomSupervisor.targetX; roomSupervisor.y = roomSupervisor.targetY; } render(); return; }
|
|
593
|
-
ensureSupervisor(); var bounds = supervisorBounds(); supervisor.targetX = Math.max(bounds.minX, Math.min(bounds.maxX, supervisor.targetX + dx * step)); supervisor.targetY = Math.max(bounds.minY, Math.min(bounds.maxY, supervisor.targetY + dy * step)); if (reducedMotion) { supervisor.x = supervisor.targetX; supervisor.y = supervisor.targetY; } render();
|
|
594
|
-
}
|
|
595
|
-
function renderRoomImage(floor, width, height){
|
|
596
|
-
width=Math.round(width);height=Math.round(height);
|
|
597
|
-
var signature=lang+'|'+T+'|'+width+'x'+height+'|'+JSON.stringify([floor.title,floor.status,floor.board,floor.props,floor.papers,floor.books,floor.clock]);
|
|
598
|
-
var cached=roomCanvases[floor.key];if(cached&&cached.signature===signature)return cached.canvas;
|
|
599
|
-
var roomCanvas=cached?cached.canvas:document.createElement('canvas');
|
|
600
|
-
if(typeof roomCanvas.getContext!=='function')return null;
|
|
601
|
-
roomCanvas.width=width;roomCanvas.height=height;var roomCtx=roomCanvas.getContext('2d');if(!roomCtx)return null;
|
|
602
|
-
var oldCanvas=canvas,oldCtx=ctx;canvas=roomCanvas;ctx=roomCtx;ctx.imageSmoothingEnabled=false;drawRoomBase(floor);canvas=oldCanvas;ctx=oldCtx;
|
|
603
|
-
roomCanvases[floor.key]={canvas:roomCanvas,signature:signature};return roomCanvas;
|
|
604
|
-
}
|
|
605
|
-
function drawOverviewRoom(floor){
|
|
606
|
-
var surface=surfaceForFloor(floor);
|
|
607
|
-
ctx.save();ctx.beginPath();ctx.rect(surface.x,surface.y,surface.width,surface.height);ctx.clip();
|
|
608
|
-
var image=renderRoomImage(floor,surface.width,surface.height);if(image)ctx.drawImage(image,surface.x,surface.y);
|
|
609
|
-
floor.actors.forEach(function(actor){drawActor(floor,actor,surface);});
|
|
610
|
-
if(floor.completedAt){ctx.fillStyle='rgba(53,90,75,.18)';ctx.fillRect(surface.x,surface.y,surface.width,surface.height);}
|
|
611
|
-
if(floor.repo){var plate=short(floor.repo,16),plateWidth=(plate.length*6+8)*U;ctx.fillStyle=repoColor(floor.repo);ctx.fillRect(surface.x+2*U,surface.y+2*U,plateWidth,13*U);ctx.strokeStyle=PALETTE[0];ctx.lineWidth=U;ctx.strokeRect(surface.x+2*U,surface.y+2*U,plateWidth,13*U);fillText(plate,surface.x+6*U,surface.y+4*U,8,PALETTE[15]);}
|
|
612
|
-
ctx.restore();
|
|
613
|
-
}
|
|
614
|
-
function render(){
|
|
615
|
-
if(!model)return;chooseRenderScale();var floor=activeRoom();
|
|
616
|
-
if(mode==='room'&&floor){
|
|
617
|
-
setCanvasSize(model.cols*T,model.rows*T);var image=renderRoomImage(floor,canvas.width,canvas.height);if(image)ctx.drawImage(image,0,0);else drawRoomBase(floor);
|
|
618
|
-
floor.actors.forEach(function(actor){drawActor(floor,actor);});drawRoomSupervisor();
|
|
619
|
-
}else{drawBackdrop(model.cols,model.rows,null);drawHallway();visibleRooms().forEach(drawOverviewRoom);drawSupervisor();if(!visibleRooms().length)fillText(t('quiet'),4*T,18*T,12);}
|
|
620
|
-
fitCanvas();
|
|
621
|
-
}
|
|
622
|
-
function renderRoomNav(rooms){
|
|
623
|
-
NAV_PAGE_SIZE=Math.max(1,Math.min(8,Math.floor((window.innerWidth-160)/120)));
|
|
624
|
-
if (mode !== "overview") { if (navSignature !== lang + "|room") { roomNav.textContent = ""; navSignature = lang + "|room"; } return; }
|
|
625
|
-
var pages = Math.max(1, Math.ceil(rooms.length / NAV_PAGE_SIZE));
|
|
626
|
-
navPage = Math.max(0, Math.min(navPage, pages - 1));
|
|
627
|
-
var signature = lang + "|overview|" + (showRecent ? "recent" : "active") + "|" + selectedKey + "|" + navPage + "|" + rooms.map(function(f){ return f.key; }).join(",");
|
|
628
|
-
if (signature === navSignature) return;
|
|
629
|
-
navSignature = signature; roomNav.textContent = "";
|
|
630
|
-
if (pages > 1) { var previous = document.createElement("button"); previous.type = "button"; previous.textContent = "‹ " + t("previous"); previous.disabled = navPage === 0; previous.addEventListener("click", function(){ navPage--; updateHeader(); }); roomNav.appendChild(previous); }
|
|
631
|
-
rooms.slice(navPage * NAV_PAGE_SIZE, navPage * NAV_PAGE_SIZE + NAV_PAGE_SIZE).forEach(function(floor, offset){ var button = document.createElement("button"); button.type = "button"; button.textContent = (navPage * NAV_PAGE_SIZE + offset + 1) + ". " + short(floor.title, 18); button.title = t("enter") + ": " + floor.title; button.setAttribute("aria-current", floor.key === selectedKey ? "true" : "false"); button.addEventListener("click", function(){ selectedKey = floor.key; enterRoom(); }); roomNav.appendChild(button); });
|
|
632
|
-
if (pages > 1) { var next = document.createElement("button"); next.type = "button"; next.textContent = t("next") + " ›"; next.disabled = navPage === pages - 1; next.addEventListener("click", function(){ navPage++; updateHeader(); }); roomNav.appendChild(next); }
|
|
633
646
|
}
|
|
634
647
|
function updateHeader(){
|
|
635
648
|
if(document.documentElement)document.documentElement.lang=lang==='zh'?'zh-Hant':'en';
|
|
636
|
-
canvas.setAttribute('aria-label',(
|
|
649
|
+
canvas.setAttribute('aria-label',t('moveHint')+' '+t('keyboardWork')+';'+t('keyboardQuestions'));
|
|
637
650
|
renderRepoFilter();
|
|
638
|
-
var rooms = visibleRooms(),
|
|
639
|
-
crumb.textContent = mode === "room" && activeRoom() ? activeRoom().title : t("overview") + " · " + rooms.length + " " + t("rooms") + " · " +
|
|
651
|
+
var rooms = visibleRooms(), count = rooms.reduce(function(n, f){ return n + f.actors.length; }, 0);
|
|
652
|
+
crumb.textContent = mode === "room" && activeRoom() ? activeRoom().title : t("overview") + " · " + rooms.length + " " + t("rooms") + " · " + count + " " + t("people");
|
|
640
653
|
backButton.hidden = mode !== "room"; backButton.textContent = "← " + t("back"); recentButton.textContent = (showRecent ? "✓ " + t("recentOn") : "▣ " + t("recent")) + (hasRecent() ? " (" + model.floors.filter(function(f){ return !!f.completedAt; }).length + ")" : "");
|
|
641
654
|
languageButton.textContent = lang === "zh" ? "繁中 / EN" : "EN / 繁中"; live.textContent = offline ? t("offline") : t("connected");
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (!
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
function
|
|
654
|
-
function
|
|
655
|
-
function changeLanguage(){ lang = lang === "zh" ? "en" : "zh"; saveLanguage(); updateHeader(); render(); }
|
|
655
|
+
overviewBox.hidden = mode !== "overview"; roomView.hidden = mode !== "room";
|
|
656
|
+
workspace.className = panelCollapsed ? "collapsed" : ""; panelToggle.textContent = panelCollapsed ? t("showList") : t("hideList"); panelToggle.setAttribute("aria-expanded", panelCollapsed ? "false" : "true");
|
|
657
|
+
renderWorkPanel();
|
|
658
|
+
}
|
|
659
|
+
function enterRoom(key){
|
|
660
|
+
if (key) selectedKey = key;
|
|
661
|
+
if (!selectedKey && visibleRooms()[0]) selectedKey = visibleRooms()[0].key;
|
|
662
|
+
if (!activeRoom()) return;
|
|
663
|
+
mode = "room"; viewer = spawnViewer(); resetLabels(); workSignature = "";
|
|
664
|
+
updateHeader(); render(); canvas.focus();
|
|
665
|
+
}
|
|
666
|
+
function goOverview(){ mode = "overview"; resetLabels(); overviewSignature = ""; workSignature = ""; updateHeader(); render(); if (overviewBox.focus) overviewBox.focus(); }
|
|
667
|
+
function changeLanguage(){ lang = lang === "zh" ? "en" : "zh"; saveLanguage(); hudSignature = ""; updateHeader(); render(); }
|
|
656
668
|
function showDialog(title, fill){
|
|
657
669
|
if (statusBox.hidden) detailPreviousFocus = document.activeElement || canvas;
|
|
670
|
+
statusBox.className = "";
|
|
658
671
|
statusBox.textContent = ""; var heading = document.createElement("h2"); heading.id = "status-title"; heading.textContent = title; statusBox.appendChild(heading);
|
|
659
672
|
var body = document.createElement("div"); body.className = "dialog-body"; statusBox.appendChild(body); fill(body);
|
|
660
673
|
var actions = document.createElement("div"); actions.className = "actions";
|
|
@@ -663,12 +676,14 @@ function showDialog(title, fill){
|
|
|
663
676
|
if (app) { app.inert = true; app.setAttribute("aria-hidden", "true"); }
|
|
664
677
|
close.focus();
|
|
665
678
|
}
|
|
666
|
-
function closeDialog(){ statusBox.hidden = true; if (app) { app.inert = false; app.removeAttribute("aria-hidden"); } var previous = detailPreviousFocus || canvas; if (!previous || previous.isConnected === false || !previous.focus) previous = canvas; dialogClose = null; detailPreviousFocus = null; if (previous && previous.focus) previous.focus(); }
|
|
679
|
+
function closeDialog(){ statusBox.hidden = true; if (app) { app.inert = false; app.removeAttribute("aria-hidden"); } var previous = detailPreviousFocus || canvas; if(previous.isConnected===false&&workList&&previous.getAttribute){var key=previous.getAttribute('data-work-key');previous=Array.prototype.slice.call(workList.querySelectorAll('button')).find(function(button){return button.getAttribute('data-work-key')===key;})||canvas;} if (!previous || previous.isConnected === false || !previous.focus) previous = canvas; dialogClose = null; detailPreviousFocus = null; if (previous && previous.focus) previous.focus(); }
|
|
667
680
|
function addLine(box, label, value){ var p = document.createElement("p"); p.textContent = label + ": " + safeText(value); box.appendChild(p); }
|
|
668
681
|
function addCommand(box, value, copyValue){
|
|
669
682
|
var row = document.createElement("p"), code = document.createElement("code"), button = document.createElement("button");
|
|
670
683
|
code.textContent = value; button.type = "button"; button.textContent = t("copy"); button.addEventListener("click", function(){
|
|
671
|
-
|
|
684
|
+
function failed(){button.textContent=t('copyFailed');button.title=t('copyHint');button.setAttribute('aria-label',t('copyHint'));}
|
|
685
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(copyValue || value).then(function(){ button.textContent = t("copied");button.removeAttribute('aria-label'); }, failed);
|
|
686
|
+
else failed();
|
|
672
687
|
}); row.appendChild(code); row.appendChild(button); box.appendChild(row);
|
|
673
688
|
}
|
|
674
689
|
function pageItems(items, box, renderItem){
|
|
@@ -682,7 +697,7 @@ function pageItems(items, box, renderItem){
|
|
|
682
697
|
}
|
|
683
698
|
function sourceFor(floor){
|
|
684
699
|
if (!snapshot) return null;
|
|
685
|
-
if (floor.kind === "run") return
|
|
700
|
+
if (floor.kind === "run") return floor.sourceIndex === null ? null : snapshot.runs[floor.sourceIndex] || null;
|
|
686
701
|
if (floor.kind === "desk") return floor.sourceIndex === null ? null : snapshot.lobby[floor.sourceIndex] || null;
|
|
687
702
|
if (floor.kind === "review") return floor.sourceIndex === null ? null : snapshot.reviews[floor.sourceIndex] || null;
|
|
688
703
|
return null;
|
|
@@ -694,8 +709,8 @@ function renderDetailItem(item, box){
|
|
|
694
709
|
if (item.kind === "heading") { var heading = document.createElement("p"); heading.textContent = item.value; box.appendChild(heading); return; }
|
|
695
710
|
addLine(box, item.label, item.value);
|
|
696
711
|
}
|
|
697
|
-
function progressText(progress){return progress ? progress.passed+'/'+(progress.total||'?')+' · '+t('verify')+' '+outcomeText(progress.verify||'pending')+' · '+t('review')+' '+outcomeText(progress.review||'pending') : t('unknown');}
|
|
698
|
-
function workItemForActor(floor,actor){return {kind:'work',floorKey:floor.key,actorKey:actor.key,label:t('owner')+': '+actor.
|
|
712
|
+
function progressText(progress){return progress ? t('checks')+' '+progress.passed+'/'+(progress.total||'?')+' · '+t('verify')+' '+outcomeText(progress.verify||'pending')+' · '+t('review')+' '+outcomeText(progress.review||'pending') : t('checks')+' '+t('unknown');}
|
|
713
|
+
function workItemForActor(floor,actor){return {kind:'work',floorKey:floor.key,actorKey:actor.key,label:t('owner')+': '+actor.id+' · '+roleText(actor.kind),value:t('currentWork')+': '+localizeText(actor.narration)+' · '+t('phase')+': '+phaseLabel(actor.phase)+' · '+t('status')+': '+completionText(floor,actor)+' · '+progressText(actor.progress)+' · '+t('pending')+': '+actor.questionCount};}
|
|
699
714
|
function workItemsFor(floor){
|
|
700
715
|
var items=[
|
|
701
716
|
{kind:'line',label:t('task'),value:floor.board.taskId==='unassigned'?t('unassigned'):floor.board.taskId},
|
|
@@ -747,6 +762,7 @@ function openQuestionList(floor){
|
|
|
747
762
|
showDialog(floor.title+' · '+t('questions'),function(box){pageItems(questionItemsFor(floor),box,renderDetailItem);});
|
|
748
763
|
}
|
|
749
764
|
function openActorDetail(floor, actor){
|
|
765
|
+
if (actor.panel) return openPanel(actor.panel, floor);
|
|
750
766
|
var source = sourceFor(floor), items = [], entries = [];
|
|
751
767
|
if (source && actor.kind !== "reviewer" && floor.kind === "run") { var found = source.agents.find(function(a){ return a.id === actor.id; }); if (found && found.diff) items = found.diff.paths || []; }
|
|
752
768
|
if (source && floor.kind === "desk" && source.diff) items = source.diff.paths || [];
|
|
@@ -755,15 +771,15 @@ function openActorDetail(floor, actor){
|
|
|
755
771
|
if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== actor.key) detailPage = 0;
|
|
756
772
|
var detailItems = [
|
|
757
773
|
{kind:"line", label:lang==='zh'?'職責':'Role', value:roleText(actor.kind)},
|
|
758
|
-
{kind:"line", label:t("status"), value:
|
|
774
|
+
{kind:"line", label:t("status"), value:completionText(floor,actor)}, {kind:"line", label:t("phase"), value:phaseLabel(actor.phase)},
|
|
759
775
|
{kind:"line", label:t("task"), value:actor.taskId === "unassigned" ? t("unassigned") : actor.taskId},
|
|
760
776
|
{kind:"line", label:t("host"), value:localizeText(actor.host)}, {kind:"line", label:t("now"), value:localizeText(actor.narration)}
|
|
761
777
|
];
|
|
762
778
|
if (actor.questionCount) detailItems.push({kind:"line", label:t("pending"), value:String(actor.questionCount)});
|
|
763
|
-
if (actor.progress) detailItems.push({kind:"line", label:t("
|
|
779
|
+
if (actor.progress) detailItems.push({kind:"line", label:t("checks"), value:progressText(actor.progress)});
|
|
764
780
|
if (actor.questionCount && floor.questions.length) floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
|
|
765
781
|
if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
|
|
766
|
-
detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: actor.key, view:'actor'}; showDialog(actor.
|
|
782
|
+
detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: actor.key, view:'actor'}; showDialog(actor.id, function(box){ pageItems(detailItems, box, renderDetailItem); });
|
|
767
783
|
}
|
|
768
784
|
function openRoomDetail(floor){
|
|
769
785
|
if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== null) detailPage = 0;
|
|
@@ -773,7 +789,7 @@ function openRoomDetail(floor){
|
|
|
773
789
|
var detailItems = [
|
|
774
790
|
{kind:"line", label:t("status"), value:statusText(floor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(floor.phase)},
|
|
775
791
|
{kind:"line", label:t("task"), value:floor.board.taskId === "unassigned" ? t("unassigned") : floor.board.taskId},
|
|
776
|
-
{kind:"line", label:t("
|
|
792
|
+
{kind:"line", label:t("checks"), value:floor.board.passed + "/" + (floor.board.total || "?")},
|
|
777
793
|
{kind:"line", label:t("verify"), value:outcomeText(floor.board.verify)}, {kind:"line", label:t("review"), value:outcomeText(floor.board.review)},
|
|
778
794
|
{kind:"line", label:t("pending"), value:String(floor.board.pending)}
|
|
779
795
|
];
|
|
@@ -786,83 +802,172 @@ function openSelectedDetail(){
|
|
|
786
802
|
if (target && target.view === 'work') return openWorkList(floor);
|
|
787
803
|
if (target && target.view === 'phase') return openPhaseWorkList(floor,target.phase);
|
|
788
804
|
if (target && target.view === 'questions') return openQuestionList(floor);
|
|
805
|
+
if (target && target.view === 'panel') return openPanel(target.kind, floor);
|
|
789
806
|
if (target && target.actorKey) { var actor = floor.actors.find(function(a){ return a.key === target.actorKey; }); if (actor) return openActorDetail(floor, actor); }
|
|
790
807
|
return openRoomDetail(floor);
|
|
791
808
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
function hitActor(event, floor, surface){
|
|
797
|
-
var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
|
|
798
|
-
for (var i = floor.actors.length - 1; i >= 0; i--) { var a=floor.actors[i],box=actorBox(a,surface);if(inside(point,{x:box.x,y:box.y,width:32*box.scale,height:48*box.scale}))return a; }
|
|
809
|
+
// ---- prop panels: the whiteboard, cubicle screens, QA board, reviewer's desk and sorting table ----
|
|
810
|
+
function hitHotspot(point){
|
|
811
|
+
for (var i = 0; i < LAYOUT.hotspots.length; i++) { var spot = LAYOUT.hotspots[i]; for (var j = 0; j < spot.rects.length; j++) { var r = spot.rects[j]; if (point.x >= r.x && point.x < r.x + r.w && point.y >= r.y && point.y < r.y + r.h) return spot.kind; } }
|
|
799
812
|
return null;
|
|
800
813
|
}
|
|
801
|
-
|
|
802
|
-
function
|
|
803
|
-
var
|
|
804
|
-
for(var i=floor.actors.length-1;i>=0;i--){var actor=floor.actors[i];if(actor.questionCount>0&&inside(point,alertGeometry(actor,surface)))return actor;}
|
|
814
|
+
/** The prop the viewer stands next to: within reach of the bottom edge of one of its rectangles. */
|
|
815
|
+
function nearHotspot(){
|
|
816
|
+
for (var i = 0; i < LAYOUT.hotspots.length; i++) { var spot = LAYOUT.hotspots[i]; for (var j = 0; j < spot.rects.length; j++) { var r = spot.rects[j]; var dx = Math.max(r.x - viewer.x, 0, viewer.x - (r.x + r.w)), dy = Math.max(r.y - viewer.y, 0, viewer.y - (r.y + r.h + 24)); if (dx + dy < 18) return spot.kind; } }
|
|
805
817
|
return null;
|
|
806
818
|
}
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
var
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
819
|
+
function el(tag, className, text){ var node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; }
|
|
820
|
+
function taskIdOf(floor, source){ var id = source && typeof source.taskId === "string" ? source.taskId : floor.board.taskId; return id && id !== "unassigned" ? id : null; }
|
|
821
|
+
function addCommands(box, commands){ if (!commands.length) return; box.appendChild(el("h3", "panel-heading", t("commands"))); commands.forEach(function(command){ addCommand(box, command); }); }
|
|
822
|
+
function panelTask(floor, source, box){
|
|
823
|
+
var phases = el("ol", "panel-phases");
|
|
824
|
+
["planning", "implementing", "verifying", "reviewing", "integrating"].forEach(function(phase){ var step = el("li", phase === floor.phase ? "now" : "", phaseLabel(phase)); if (phase === floor.phase) step.setAttribute("aria-current", "step"); phases.appendChild(step); });
|
|
825
|
+
box.appendChild(phases);
|
|
826
|
+
var criteria = floor.criteria || [], groups = [["todo", t("critPending"), function(c){ return c.status === null; }], ["fail", t("critFail"), function(c){ return c.status === "FAIL" || c.status === "UNKNOWN"; }], ["pass", t("critPass"), function(c){ return c.status === "PASS"; }]];
|
|
827
|
+
if (!criteria.length) box.appendChild(el("p", "", t("noCriteria")));
|
|
828
|
+
else {
|
|
829
|
+
var columns = el("div", "panel-notes");
|
|
830
|
+
groups.forEach(function(group){
|
|
831
|
+
var items = criteria.filter(group[2]), column = el("div", "panel-column");
|
|
832
|
+
column.appendChild(el("h3", "panel-heading", group[1] + " · " + items.length));
|
|
833
|
+
items.forEach(function(c){ var note = el("div", "note " + group[0]); note.appendChild(el("strong", "", c.id)); note.appendChild(el("span", "", c.description)); column.appendChild(note); });
|
|
834
|
+
columns.appendChild(column);
|
|
835
|
+
});
|
|
836
|
+
box.appendChild(columns);
|
|
837
|
+
}
|
|
838
|
+
floor.questions.forEach(function(question){ box.appendChild(el("p", "panel-question", "! " + question.prompt)); });
|
|
839
|
+
addCommands(box, source && source.commands ? source.commands : []);
|
|
840
|
+
}
|
|
841
|
+
function panelDiff(floor, source, box){
|
|
842
|
+
var diff = source && source.diff ? source.diff : null;
|
|
843
|
+
if (!diff || !diff.files) { box.appendChild(el("p", "", t("noChanges"))); return; }
|
|
844
|
+
box.appendChild(el("p", "panel-meta", (source.branch ? source.branch + " · " : "") + diff.files + " " + t("filesCount") + " · +" + diff.insertions + " −" + diff.deletions));
|
|
845
|
+
var list = el("ul", "panel-files");
|
|
846
|
+
diff.paths.slice(0, 60).forEach(function(path){ list.appendChild(el("li", path === diff.recent ? "recent" : "", (path === diff.recent ? "▶ " : "") + path)); });
|
|
847
|
+
box.appendChild(list);
|
|
848
|
+
if (diff.paths.length > 60) box.appendChild(el("p", "panel-meta", "+" + (diff.paths.length - 60)));
|
|
849
|
+
}
|
|
850
|
+
function panelVerify(floor, source, box){
|
|
851
|
+
var criteria = floor.criteria || [], taskId = taskIdOf(floor, source);
|
|
852
|
+
box.appendChild(el("p", "panel-meta", t("verify") + " " + outcomeText(floor.board.verify)));
|
|
853
|
+
if (!criteria.length) box.appendChild(el("p", "", t("noCriteria")));
|
|
854
|
+
var list = el("ul", "panel-leds");
|
|
855
|
+
criteria.forEach(function(c){
|
|
856
|
+
var row = el("li", "led-row " + (c.status ? c.status.toLowerCase() : "pending"));
|
|
857
|
+
row.appendChild(el("strong", "", c.id + " · " + criterionStatusText(c.status)));
|
|
858
|
+
row.appendChild(el("span", "", c.description));
|
|
859
|
+
var meta = []; if (c.failureClass && c.status !== "PASS") meta.push(c.failureClass); if (c.exitCode !== null && c.exitCode !== undefined && c.status !== "PASS") meta.push("exit " + c.exitCode); if (c.finishedAt) meta.push(c.finishedAt.replace("T", " ").slice(0, 16));
|
|
860
|
+
if (meta.length) row.appendChild(el("small", "", meta.join(" · ")));
|
|
861
|
+
if (c.output) row.appendChild(el("pre", "panel-output", c.output));
|
|
862
|
+
list.appendChild(row);
|
|
863
|
+
});
|
|
864
|
+
box.appendChild(list);
|
|
865
|
+
if (taskId) addCommands(box, ["agent-ops verify --task " + taskId]);
|
|
866
|
+
}
|
|
867
|
+
function panelReview(floor, source, box){
|
|
868
|
+
var review = source && source.review ? source.review : null, taskId = taskIdOf(floor, source);
|
|
869
|
+
if (!review) box.appendChild(el("p", "", t("notReviewed")));
|
|
870
|
+
else {
|
|
871
|
+
box.appendChild(el("p", "stamp " + review.status.toLowerCase(), review.status));
|
|
872
|
+
box.appendChild(el("p", "panel-meta", review.createdAt.replace("T", " ").slice(0, 16) + (review.reason ? " · " + review.reason : "")));
|
|
873
|
+
review.rounds.forEach(function(round, index){
|
|
874
|
+
var part = el("section", "round");
|
|
875
|
+
part.appendChild(el("h3", "panel-heading", (index === 0 ? t("firstRound") : t("recheck")) + " · " + round.target));
|
|
876
|
+
if (round.summary) part.appendChild(el("p", "", round.summary));
|
|
877
|
+
if (round.findings.length) {
|
|
878
|
+
var findings = el("ul", "panel-findings");
|
|
879
|
+
round.findings.forEach(function(f){ var item = el("li", f.blocking ? "blocking" : ""); item.appendChild(el("strong", "", "[" + f.severity + (f.blocking ? " · " + t("blocking") : "") + "] " + f.title)); if (f.details) item.appendChild(el("p", "", f.details)); if (f.recommendation) item.appendChild(el("p", "advice", "→ " + f.recommendation)); findings.appendChild(item); });
|
|
880
|
+
part.appendChild(findings);
|
|
881
|
+
}
|
|
882
|
+
box.appendChild(part);
|
|
883
|
+
});
|
|
884
|
+
if (review.refuted !== null) box.appendChild(el("p", "panel-meta", review.refuted ? t("refutedYes") : t("refutedNo")));
|
|
885
|
+
}
|
|
886
|
+
if (taskId) addCommands(box, ["agent-ops review --task " + taskId + " --yes"]);
|
|
887
|
+
}
|
|
888
|
+
function panelIntegration(floor, source, box){
|
|
889
|
+
var facts = el("dl", "panel-facts"), add = function(label, value){ facts.appendChild(el("dt", "", label)); facts.appendChild(el("dd", "", value)); };
|
|
890
|
+
add(t("worktree"), source && source.name ? source.name : t("unknown"));
|
|
891
|
+
add(t("branch"), source && source.branch ? source.branch : t("unknown"));
|
|
892
|
+
add(t("base"), source && source.base ? source.base : t("unknown"));
|
|
893
|
+
add(t("ahead"), source && typeof source.ahead === "number" ? String(source.ahead) : t("unknown"));
|
|
894
|
+
add(t("files"), source && source.diff ? String(source.diff.files) : "0");
|
|
895
|
+
box.appendChild(facts);
|
|
896
|
+
var verify = floor.board.verify, review = floor.board.review, steps = el("ol", "panel-steps");
|
|
897
|
+
[[t("commitStep"), source && source.ahead > 0 ? "done" : "pending"], [t("verify"), verify === "PASS" ? "done" : verify === "FAIL" ? "failed" : "pending"],
|
|
898
|
+
[t("review"), review === "PASS" ? "done" : review === "FAIL" ? "failed" : "pending"], [t("finishStep"), floor.completedAt ? "done" : "pending"]]
|
|
899
|
+
.forEach(function(step){ var item = el("li", step[1], (step[1] === "done" ? "✓ " : step[1] === "failed" ? "✗ " : "○ ") + step[0]); item.setAttribute("data-state", step[1]); steps.appendChild(item); });
|
|
900
|
+
box.appendChild(steps);
|
|
901
|
+
if (source && source.name && source.branch && source.branch !== "(no worktree)") addCommands(box, ["agent-ops worktree finish " + source.name]);
|
|
902
|
+
}
|
|
903
|
+
var PANELS = {task: panelTask, diff: panelDiff, verify: panelVerify, review: panelReview, integration: panelIntegration};
|
|
904
|
+
function openPanel(kind, floor){
|
|
905
|
+
var source = sourceFor(floor);
|
|
906
|
+
detailSignature = sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: null, view: "panel", kind: kind};
|
|
907
|
+
showDialog(t("panel_" + kind) + " · " + floor.title, function(box){ PANELS[kind](floor, source, box); });
|
|
908
|
+
statusBox.className = "panel panel-" + kind;
|
|
909
|
+
}
|
|
910
|
+
// ---- input: the viewer walks inside a room; walls and furniture stop them ----
|
|
911
|
+
function freeAt(p){
|
|
912
|
+
var cx = Math.round(p.x / 2), cy = Math.round(p.y / 2);
|
|
913
|
+
return cx >= 0 && cy >= 0 && cx < GRID.cols && cy < GRID.rows && GRID.free[cy * GRID.cols + cx] === 1;
|
|
914
|
+
}
|
|
915
|
+
function moveViewer(dx, dy){
|
|
916
|
+
viewer.dir = dx ? (dx < 0 ? "left" : "right") : (dy < 0 ? "up" : "down"); viewer.path = [];
|
|
917
|
+
for (var i = 0; i < 4; i++) {
|
|
918
|
+
var next = {x: viewer.x + dx * 2, y: viewer.y + dy * 2};
|
|
919
|
+
if (!freeAt(next)) break;
|
|
920
|
+
viewer.x = next.x; viewer.y = next.y; viewer.step += 2;
|
|
921
|
+
}
|
|
922
|
+
render();
|
|
923
|
+
}
|
|
924
|
+
function walkTo(point){
|
|
925
|
+
var path = findPath(GRID, {x: viewer.x, y: viewer.y}, point);
|
|
926
|
+
if (!path) return false;
|
|
927
|
+
if (reducedMotion) { var end = path[path.length - 1]; viewer.x = end.x; viewer.y = end.y; viewer.path = []; }
|
|
928
|
+
else viewer.path = path.slice(1);
|
|
929
|
+
render(); return true;
|
|
821
930
|
}
|
|
822
|
-
function
|
|
823
|
-
var
|
|
824
|
-
|
|
825
|
-
|
|
931
|
+
function canvasPoint(event){
|
|
932
|
+
var rect = canvas.getBoundingClientRect(), w = rect.width || canvas.width || ROOM_W, h = rect.height || canvas.height || ROOM_H;
|
|
933
|
+
return {x: Math.floor((event.clientX - rect.left) * ROOM_W / w), y: Math.floor((event.clientY - rect.top) * ROOM_H / h)};
|
|
934
|
+
}
|
|
935
|
+
function hitFigure(point){
|
|
936
|
+
for (var i = lastFigures.length - 1; i >= 0; i--) { var f = lastFigures[i]; if (point.x >= f.x + 6 && point.x < f.x + 28 && point.y >= f.y && point.y < f.y + 50) return f; }
|
|
826
937
|
return null;
|
|
827
938
|
}
|
|
828
939
|
canvas.addEventListener("click", function(event){
|
|
829
|
-
if (!
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
var work=hitWorkTarget(event,floor,surface);if(work){if(work.actor)openActorDetail(floor,work.actor);else if(work.phase)openPhaseWorkList(floor,work.phase);return;}
|
|
836
|
-
enterRoom();return;
|
|
837
|
-
}
|
|
838
|
-
var room = activeRoom(); if (!room) return; var roomSurface=surfaceForFloor(room), actor = hitActor(event, room, roomSurface); if (actor) openActorDetail(room, actor); else {
|
|
839
|
-
var roomBadge=hitQuestionBadge(event,room,roomSurface);if(roomBadge){openQuestionList(room);return;}
|
|
840
|
-
var roomBoard=hitWorkBoard(event,room,roomSurface);if(roomBoard==='questions'){openQuestionList(room);return;}if(roomBoard){openWorkList(room);return;}
|
|
841
|
-
var roomWork=hitWorkTarget(event,room,roomSurface);if(roomWork){if(roomWork.actor)openActorDetail(room,roomWork.actor);else if(roomWork.phase)openPhaseWorkList(room,roomWork.phase);else openRoomDetail(room);return;}
|
|
842
|
-
openRoomDetail(room);
|
|
843
|
-
}
|
|
940
|
+
var floor = activeRoom(); if (mode !== "room" || !floor) return;
|
|
941
|
+
// Props first: people sitting at a desk or screen must not hide its panel.
|
|
942
|
+
var point = canvasPoint(event), spot = hitHotspot(point), figure = hitFigure(point);
|
|
943
|
+
if (spot) { openPanel(spot, floor); return; }
|
|
944
|
+
if (figure) { openActorDetail(floor, figure.actor); return; }
|
|
945
|
+
walkTo({x: Math.round(point.x / 2) * 2, y: Math.round(point.y / 2) * 2});
|
|
844
946
|
});
|
|
947
|
+
function keyboardFloor(){ return activeRoom() || roomByKey(selectedKey) || orderedRooms()[0]; }
|
|
948
|
+
function sharedKeys(event){
|
|
949
|
+
if (event.key === "Escape" || event.key === "Backspace") { event.preventDefault(); if (!statusBox.hidden) closeDialog(); else if (mode === "room") goOverview(); return true; }
|
|
950
|
+
var floor = keyboardFloor();
|
|
951
|
+
if ((event.key === "l" || event.key === "L") && floor) { event.preventDefault(); openWorkList(floor); return true; }
|
|
952
|
+
if ((event.key === "q" || event.key === "Q") && floor) { event.preventDefault(); openQuestionList(floor); return true; }
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
845
955
|
canvas.addEventListener("keydown", function(event){
|
|
846
|
-
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
847
|
-
|
|
848
|
-
var
|
|
849
|
-
if (event.key
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
if (mode === "overview" || mode === "room") {
|
|
854
|
-
var move = {ArrowLeft:[-1,0], ArrowRight:[1,0], ArrowUp:[0,-1], ArrowDown:[0,1], a:[-1,0], d:[1,0], w:[0,-1], s:[0,1], A:[-1,0], D:[1,0], W:[0,-1], S:[0,1]}[event.key];
|
|
855
|
-
if (move) { event.preventDefault(); moveSupervisor(move[0], move[1]); return; }
|
|
856
|
-
}
|
|
857
|
-
var rooms = visibleRooms(); if (!rooms.length) return;
|
|
858
|
-
var index = Math.max(0, rooms.findIndex(function(f){ return f.key === selectedKey; }));
|
|
859
|
-
if (event.key === "ArrowRight" || event.key === "ArrowDown") { event.preventDefault(); selectedKey = rooms[(index + 1) % rooms.length].key; updateHeader(); render(); }
|
|
860
|
-
if (event.key === "ArrowLeft" || event.key === "ArrowUp") { event.preventDefault(); selectedKey = rooms[(index - 1 + rooms.length) % rooms.length].key; updateHeader(); render(); }
|
|
861
|
-
if (event.key === "Enter" && mode === "overview") { event.preventDefault(); enterRoom(); }
|
|
862
|
-
if (event.key === "Enter" && mode === "room") { event.preventDefault(); openSelectedDetail(); }
|
|
956
|
+
if (event.ctrlKey || event.metaKey || event.altKey || sharedKeys(event)) return;
|
|
957
|
+
var floor = activeRoom(); if (mode !== "room" || !floor) return;
|
|
958
|
+
var phaseKeys = {"1": "planning", "2": "implementing", "3": "verifying", "4": "reviewing", "5": "integrating"};
|
|
959
|
+
if (phaseKeys[event.key]) { event.preventDefault(); openPhaseWorkList(floor, phaseKeys[event.key]); return; }
|
|
960
|
+
var move = {ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1], a: [-1, 0], d: [1, 0], w: [0, -1], s: [0, 1], A: [-1, 0], D: [1, 0], W: [0, -1], S: [0, 1]}[event.key];
|
|
961
|
+
if (move) { event.preventDefault(); moveViewer(move[0], move[1]); return; }
|
|
962
|
+
if (event.key === "Enter") { event.preventDefault(); var near = nearestActor(lastFigures), prop = nearHotspot(); if (near) openActorDetail(floor, near.actor); else if (prop) openPanel(prop, floor); else openRoomDetail(floor); }
|
|
863
963
|
});
|
|
864
|
-
|
|
865
|
-
|
|
964
|
+
overviewBox.addEventListener("keydown", function(event){
|
|
965
|
+
if (event.ctrlKey || event.metaKey || event.altKey || sharedKeys(event)) return;
|
|
966
|
+
if (/^[1-9]$/u.test(event.key)) { var floor = (panelCollapsed ? pageRooms(pageInView()) : orderedRooms())[Number(event.key) - 1]; if (floor) { event.preventDefault(); enterRoom(floor.key); } }
|
|
967
|
+
});
|
|
968
|
+
repoSelect.addEventListener("change", function(){ repoFilter = repoSelect.value; if (activeRoom() && repoFilter && activeRoom().repo !== repoFilter) mode = "overview"; if (!selectedKey || visibleRooms().every(function(f){ return f.key !== selectedKey; })) selectedKey = visibleRooms()[0] && visibleRooms()[0].key; updateHeader(); render(); });
|
|
969
|
+
backButton.addEventListener("click", goOverview);
|
|
970
|
+
panelToggle.addEventListener("click", function(){ panelCollapsed = !panelCollapsed; try { localStorage.setItem("agent-office-list", panelCollapsed ? "collapsed" : "open"); } catch (_) {} updateHeader(); render(); }); recentButton.addEventListener("click", function(){ showRecent = !showRecent; if (!showRecent && activeRoom() && activeRoom().completedAt) mode = "overview"; updateHeader(); render(); });
|
|
866
971
|
languageButton.addEventListener("click", changeLanguage); statusBox.addEventListener("keydown", function(event){
|
|
867
972
|
if (event.key === "Escape") { event.preventDefault(); closeDialog(); return; }
|
|
868
973
|
if (event.key !== "Tab") return;
|
|
@@ -872,18 +977,28 @@ languageButton.addEventListener("click", changeLanguage); statusBox.addEventList
|
|
|
872
977
|
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
873
978
|
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
874
979
|
});
|
|
875
|
-
function poll(){ fetch("snapshot.json" + location.search, {cache:"no-store"}).then(function(r){ if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(function(s){ snapshot = s; model = sceneModel(s); offline = false; var selectedMissing=mode==='room'&&!!selectedKey&&!roomByKey(selectedKey),dialogMissing=!statusBox.hidden&&!!detailTarget&&!roomByKey(detailTarget.floorKey); if(selectedMissing||dialogMissing){if(dialogMissing)detailPreviousFocus=canvas;if(!statusBox.hidden)closeDialog();detailTarget=null;detailPage=0;detailSignature="";mode='overview';} if (!selectedKey || !roomByKey(selectedKey)) selectedKey = visibleRooms()[0] && visibleRooms()[0].key; updateHeader(); render(); if (!statusBox.hidden && detailTarget && roomByKey(detailTarget.floorKey)) { var signature=sourceSignature(roomByKey(detailTarget.floorKey)); if(signature!==detailSignature){detailSignature=signature;openSelectedDetail();} } }, function(){ offline = true; updateHeader(); }); }
|
|
876
|
-
function loop(){
|
|
877
|
-
updateHeader(); poll(); setInterval(poll, 2000); window.addEventListener("resize", render); requestAnimationFrame(loop);
|
|
980
|
+
function poll(){ fetch("snapshot.json" + location.search, {cache:"no-store"}).then(function(r){ if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(function(s){ snapshot = s; model = sceneModel(s); syncPeople(); offline = false; var selectedMissing=mode==='room'&&!!selectedKey&&!roomByKey(selectedKey),dialogMissing=!statusBox.hidden&&!!detailTarget&&!roomByKey(detailTarget.floorKey); if(selectedMissing||dialogMissing){if(dialogMissing)detailPreviousFocus=canvas;if(!statusBox.hidden)closeDialog();detailTarget=null;detailPage=0;detailSignature="";mode='overview';resetLabels();} if (!selectedKey || !roomByKey(selectedKey)) selectedKey = visibleRooms()[0] && visibleRooms()[0].key; updateHeader(); render(); if (!statusBox.hidden && detailTarget && roomByKey(detailTarget.floorKey)) { var signature=sourceSignature(roomByKey(detailTarget.floorKey)); if(signature!==detailSignature){detailSignature=signature;openSelectedDetail();} } }, function(){ offline = true; updateHeader(); }); }
|
|
981
|
+
function loop(){ tick(); requestAnimationFrame(loop); }
|
|
982
|
+
updateHeader(); poll(); setInterval(poll, 2000); window.addEventListener("resize", function(){updateHeader();render();}); requestAnimationFrame(loop);
|
|
878
983
|
`;
|
|
984
|
+
let data = null;
|
|
985
|
+
/** Art, layout and the shared pure functions, serialised once for every page. */
|
|
986
|
+
function pageData() {
|
|
987
|
+
data ??= {
|
|
988
|
+
__PALETTE__: JSON.stringify(ROOM_PALETTE), __SPRITES__: JSON.stringify(roomSprites()), __PEOPLE__: JSON.stringify(personSprites()),
|
|
989
|
+
__LAYOUT__: JSON.stringify(officeLayout()), __SCENE__: sceneModel.toString(), __AVATAR_COLORS__: avatarColors.toString(),
|
|
990
|
+
__WALK__: walkGrid.toString(), __PATH__: findPath.toString(), __NOTE_POS__: notePosition.toString()
|
|
991
|
+
};
|
|
992
|
+
return data;
|
|
993
|
+
}
|
|
879
994
|
/** One inline page; the nonce binds its only script and style under the server's CSP. */
|
|
880
995
|
export function officePage(nonce) {
|
|
881
|
-
const
|
|
882
|
-
|
|
996
|
+
const values = pageData();
|
|
997
|
+
const script = Object.keys(values).reduce((text, key) => text.replace(key, () => values[key]), CLIENT);
|
|
883
998
|
return `<!doctype html>
|
|
884
999
|
<html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
885
1000
|
<meta name="referrer" content="no-referrer"><title>agent-ops Office · Preview</title><style nonce="${nonce}">${STYLE}</style></head>
|
|
886
1001
|
<body><div id="app"><header id="header"><div id="brand">agent-ops Office · Preview</div><div id="crumb">Office overview</div><button id="back" type="button" hidden>← Back to overview</button><button id="recent" type="button">▣ Recently completed</button><select id="repo-filter" hidden></select><button id="language" type="button">繁中 / EN</button><span id="live" aria-live="polite">● Connected</span></header>
|
|
887
|
-
<main id="wrap"><canvas id="office" tabindex="0" width="576" height="
|
|
1002
|
+
<main id="workspace"><section id="wrap" aria-label="Pixel office"><div id="overview" tabindex="-1"></div><div id="room-view" hidden><div id="hud"></div><div id="stage"><canvas id="office" tabindex="0" width="576" height="320" aria-label="Office room"></canvas><div id="labels"></div></div></div></section><aside id="work-panel" aria-labelledby="work-heading"><div id="panel-head"><h2 id="work-heading">Work list</h2><button id="panel-toggle" type="button" aria-controls="work-list" aria-expanded="true">Hide list »</button></div><p id="work-summary"></p><div id="work-list"></div></aside></main></div>
|
|
888
1003
|
<section id="status" role="dialog" aria-modal="true" aria-labelledby="status-title" tabindex="-1" hidden></section><script nonce="${nonce}">${script}</script></body></html>`;
|
|
889
1004
|
}
|