oficina-opencode 3.1.0 → 3.1.1
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 +1 -1
- package/commands/oficina.md +7 -6
- package/dashboard/.dom-check.html +1257 -0
- package/dashboard/README-DASHBOARD.md +64 -0
- package/dashboard/assets/factory.css +1543 -0
- package/dashboard/assets/tokens.css +208 -0
- package/dashboard/build-kanban-data.mjs +279 -0
- package/dashboard/dashboard.css +753 -0
- package/dashboard/dashboard.html +121 -0
- package/dashboard/dashboard.js +610 -0
- package/dashboard/data/snapshot.json +883 -0
- package/dashboard/debate.html +134 -0
- package/dashboard/factory/build-snapshot.mjs +89 -0
- package/dashboard/factory/factory-app.js +1185 -0
- package/dashboard/factory/factory-core.js +541 -0
- package/dashboard/factory/snapshot.js +850 -0
- package/dashboard/factory.html +350 -0
- package/dashboard/kanban-data.json +671 -0
- package/dashboard/kanban-source.js +643 -0
- package/dashboard/kanban.check.mjs +454 -0
- package/dashboard/kanban.html +1257 -0
- package/dashboard/leadership.html +1008 -0
- package/dashboard/lib/debate-app.js +1149 -0
- package/dashboard/lib/debate-core.js +698 -0
- package/dashboard/lib/debate-data.js +163 -0
- package/dashboard/lib/debate-demo.js +110 -0
- package/dashboard/lib/debate.css +1155 -0
- package/dashboard/lib/leadership-model.js +946 -0
- package/dashboard/probe.mjs +35 -0
- package/dashboard/scripts/serve.mjs +122 -0
- package/dashboard/scripts/snapshot-data.mjs +93 -0
- package/dashboard/scripts/snapshot.mjs +28 -0
- package/dashboard/server.mjs +242 -0
- package/dashboard/shot.mjs +54 -0
- package/dashboard/shots/desktop-gates.png +0 -0
- package/dashboard/shots/desktop-light.png +0 -0
- package/dashboard/shots/desktop-real.png +0 -0
- package/dashboard/shots/mobile.png +0 -0
- package/dashboard/test/factory-core.test.mjs +244 -0
- package/dashboard/tests/debate-core.test.mjs +336 -0
- package/dashboard/tests/leadership-model.test.mjs +404 -0
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/dashboard.d.ts +33 -0
- package/dist/core/dashboard.d.ts.map +1 -0
- package/dist/core/dashboard.js +115 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +11 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/environment.d.ts +28 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +80 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/systemPrompt.d.ts.map +1 -1
- package/dist/core/systemPrompt.js +3 -0
- package/dist/core/systemPrompt.js.map +1 -1
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +26 -22
- package/dist/tools/index.js.bak +972 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/v2.d.ts +5 -0
- package/dist/v2.d.ts.map +1 -1
- package/dist/v2.js +57 -5
- package/dist/v2.js.bak +517 -0
- package/dist/v2.js.map +1 -1
- package/docs/QUE-ES.md +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* debate-app.js — render e interacción del panel de debate (T-05).
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidades: leer el snapshot (live / archivos / demo), construir los
|
|
5
|
+
* hilos con DebateCore, pintar burbujas anidadas, dejar el estado visible en el
|
|
6
|
+
* DOM y anunciar novedades por una ARIA live region. Toda la lógica de datos
|
|
7
|
+
* vive en debate-core.js (testeada); acá sólo hay DOM.
|
|
8
|
+
*/
|
|
9
|
+
(function (root, document) {
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
var Core = root.DebateCore;
|
|
13
|
+
var Data = root.DebateData;
|
|
14
|
+
if (!Core || !Data) {
|
|
15
|
+
if (document && document.body) {
|
|
16
|
+
document.body.textContent = "Faltan debate-core.js o debate-data.js.";
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var POLL_MS = 1000;
|
|
22
|
+
var MAX_BACKOFF_MS = 15000;
|
|
23
|
+
var LONG_TEXT = 220;
|
|
24
|
+
|
|
25
|
+
var state = {
|
|
26
|
+
mode: "server",
|
|
27
|
+
apiBase: "",
|
|
28
|
+
srcUrl: null,
|
|
29
|
+
reason: "",
|
|
30
|
+
snapshot: Core.readSnapshot(null),
|
|
31
|
+
built: { threads: [], unlinked: [], messages: [] },
|
|
32
|
+
open: new Set(),
|
|
33
|
+
closed: new Set(),
|
|
34
|
+
expanded: new Set(),
|
|
35
|
+
seen: Object.create(null),
|
|
36
|
+
filters: { status: "all", query: "", agent: "" },
|
|
37
|
+
paused: false,
|
|
38
|
+
loadedOnce: false,
|
|
39
|
+
lastUpdateAt: null,
|
|
40
|
+
lastSignature: "",
|
|
41
|
+
backoff: POLL_MS,
|
|
42
|
+
error: null,
|
|
43
|
+
focusKey: null,
|
|
44
|
+
timer: null,
|
|
45
|
+
demo: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
var ui = {};
|
|
49
|
+
|
|
50
|
+
// ------------------------------------------------------------- DOM utils
|
|
51
|
+
|
|
52
|
+
function h(tag, attrs) {
|
|
53
|
+
var node = document.createElement(tag);
|
|
54
|
+
if (attrs) {
|
|
55
|
+
Object.keys(attrs).forEach(function (key) {
|
|
56
|
+
var value = attrs[key];
|
|
57
|
+
if (value === null || value === undefined || value === false) return;
|
|
58
|
+
if (key === "class") node.className = value;
|
|
59
|
+
else if (key === "text") node.textContent = String(value);
|
|
60
|
+
else if (key === "style") node.setAttribute("style", value);
|
|
61
|
+
else if (key === "dataset") {
|
|
62
|
+
Object.keys(value).forEach(function (dataKey) {
|
|
63
|
+
node.dataset[dataKey] = String(value[dataKey]);
|
|
64
|
+
});
|
|
65
|
+
} else if (value === true) node.setAttribute(key, "");
|
|
66
|
+
else node.setAttribute(key, String(value));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (var i = 2; i < arguments.length; i += 1) {
|
|
70
|
+
appendChild(node, arguments[i]);
|
|
71
|
+
}
|
|
72
|
+
return node;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function appendChild(parent, child) {
|
|
76
|
+
if (child === null || child === undefined || child === false) return;
|
|
77
|
+
if (Array.isArray(child)) {
|
|
78
|
+
child.forEach(function (item) {
|
|
79
|
+
appendChild(parent, item);
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
84
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
parent.appendChild(child);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function clear(node) {
|
|
91
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function safeId(value) {
|
|
95
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function agentInfo(agentId) {
|
|
99
|
+
var fromSnapshot = state.snapshot.agents && state.snapshot.agents[agentId];
|
|
100
|
+
if (fromSnapshot) {
|
|
101
|
+
return {
|
|
102
|
+
id: agentId,
|
|
103
|
+
name: fromSnapshot.displayName || fromSnapshot.role || agentId,
|
|
104
|
+
role: fromSnapshot.role || "",
|
|
105
|
+
state: fromSnapshot.state || "",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { id: agentId, name: agentId, role: "", state: "" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cardTitle(subject) {
|
|
112
|
+
var card = state.snapshot.cards && state.snapshot.cards[subject];
|
|
113
|
+
return card && card.title ? card.title : "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function avatar(agentId, small) {
|
|
117
|
+
var info = agentInfo(agentId);
|
|
118
|
+
return h(
|
|
119
|
+
"span",
|
|
120
|
+
{
|
|
121
|
+
class: "avatar" + (small ? " avatar-sm" : ""),
|
|
122
|
+
style: "--h:" + Core.agentHue(agentId),
|
|
123
|
+
title: info.name + (info.role ? " (" + info.role + ")" : ""),
|
|
124
|
+
"aria-hidden": "true",
|
|
125
|
+
},
|
|
126
|
+
Core.initials(info.name),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function timeNode(iso, extraClass) {
|
|
131
|
+
return h("time", {
|
|
132
|
+
class: "time" + (extraClass ? " " + extraClass : ""),
|
|
133
|
+
datetime: iso,
|
|
134
|
+
"data-ts": iso,
|
|
135
|
+
title: Core.formatDateTime(iso),
|
|
136
|
+
text: Core.relativeTime(iso, Date.now()),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ------------------------------------------------------------ snapshot
|
|
141
|
+
|
|
142
|
+
function applySnapshot(snapshot, options) {
|
|
143
|
+
var opts = options || {};
|
|
144
|
+
var previousSignature = state.lastSignature;
|
|
145
|
+
state.snapshot = snapshot;
|
|
146
|
+
state.built = Core.buildThreads(snapshot.messages || [], { now: Date.now() });
|
|
147
|
+
state.lastUpdateAt = Date.now();
|
|
148
|
+
state.error = null;
|
|
149
|
+
state.demo = !!(snapshot.meta && (snapshot.meta.source === "demo" || snapshot.demo));
|
|
150
|
+
|
|
151
|
+
var fresh = [];
|
|
152
|
+
if (state.loadedOnce) {
|
|
153
|
+
fresh = Core.newMessages(state.seen, state.built.threads, {
|
|
154
|
+
onlyOpen: true,
|
|
155
|
+
openThreads: openMap(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
var seen = Object.create(null);
|
|
159
|
+
Object.keys(state.seen).forEach(function (id) {
|
|
160
|
+
seen[id] = true;
|
|
161
|
+
});
|
|
162
|
+
Object.keys(Core.idsMap(state.built.messages)).forEach(function (id) {
|
|
163
|
+
seen[id] = true;
|
|
164
|
+
});
|
|
165
|
+
state.seen = seen;
|
|
166
|
+
|
|
167
|
+
if (!state.loadedOnce) {
|
|
168
|
+
state.loadedOnce = true;
|
|
169
|
+
// Primer pintado: abrimos lo que está vivo para que se vea el debate.
|
|
170
|
+
state.built.threads.forEach(function (thread) {
|
|
171
|
+
if (thread.status !== "completed" && state.open.size < 3) state.open.add(thread.id);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
syncAgentFilter();
|
|
176
|
+
var signature = Core.snapshotSignature(state.built.threads) + "|" + state.built.unlinked.length;
|
|
177
|
+
if (signature !== previousSignature) {
|
|
178
|
+
state.lastSignature = signature;
|
|
179
|
+
render();
|
|
180
|
+
} else {
|
|
181
|
+
renderStats();
|
|
182
|
+
renderConnection();
|
|
183
|
+
renderTimes();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (fresh.length > 0) {
|
|
187
|
+
announce(fresh.slice(0, 3).map(Core.announcementFor).join(". "));
|
|
188
|
+
}
|
|
189
|
+
if (opts.notice) announce(opts.notice, true);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function openMap() {
|
|
193
|
+
var map = Object.create(null);
|
|
194
|
+
state.built.threads.forEach(function (thread) {
|
|
195
|
+
map[thread.id] = isOpen(thread);
|
|
196
|
+
});
|
|
197
|
+
return map;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isOpen(thread) {
|
|
201
|
+
if (state.closed.has(thread.id)) return false;
|
|
202
|
+
if (state.open.has(thread.id)) return true;
|
|
203
|
+
return thread.status !== "completed";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ------------------------------------------------------------- anuncios
|
|
207
|
+
|
|
208
|
+
function announce(text, assertive) {
|
|
209
|
+
if (!text || !ui.live) return;
|
|
210
|
+
ui.live.setAttribute("aria-live", assertive ? "assertive" : "polite");
|
|
211
|
+
// Forzar el re-anuncio cuando el texto se repite.
|
|
212
|
+
ui.live.textContent = "";
|
|
213
|
+
var message = text;
|
|
214
|
+
setTimeout(function () {
|
|
215
|
+
if (ui.live) ui.live.textContent = message;
|
|
216
|
+
}, 60);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// -------------------------------------------------------------- render
|
|
220
|
+
|
|
221
|
+
function render() {
|
|
222
|
+
renderConnection();
|
|
223
|
+
renderStats();
|
|
224
|
+
renderFilters();
|
|
225
|
+
renderBanner();
|
|
226
|
+
renderThreads();
|
|
227
|
+
renderFooter();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderConnection() {
|
|
231
|
+
if (!ui.conn) return;
|
|
232
|
+
var label = "en vivo";
|
|
233
|
+
var tone = "live";
|
|
234
|
+
if (state.mode === "demo") {
|
|
235
|
+
label = "demo";
|
|
236
|
+
tone = "demo";
|
|
237
|
+
} else if (state.mode === "files") {
|
|
238
|
+
label = "archivos";
|
|
239
|
+
tone = "files";
|
|
240
|
+
} else if (state.paused) {
|
|
241
|
+
label = "pausado";
|
|
242
|
+
tone = "paused";
|
|
243
|
+
} else if (state.error) {
|
|
244
|
+
label = "sin conexión";
|
|
245
|
+
tone = "error";
|
|
246
|
+
}
|
|
247
|
+
ui.conn.textContent = label;
|
|
248
|
+
ui.conn.className = "conn conn-" + tone;
|
|
249
|
+
ui.conn.setAttribute(
|
|
250
|
+
"title",
|
|
251
|
+
state.mode === "server"
|
|
252
|
+
? "Leyendo /api/debate de " + (state.apiBase || location.origin) + " cada " + POLL_MS + "ms"
|
|
253
|
+
: state.mode === "files"
|
|
254
|
+
? "Datos cargados desde archivos JSON locales"
|
|
255
|
+
: "Datos sintéticos de demostración",
|
|
256
|
+
);
|
|
257
|
+
if (ui.updated) {
|
|
258
|
+
ui.updated.textContent = state.lastUpdateAt
|
|
259
|
+
? "actualizado " + Core.relativeTime(new Date(state.lastUpdateAt).toISOString(), Date.now())
|
|
260
|
+
: "sin datos";
|
|
261
|
+
}
|
|
262
|
+
if (ui.pauseBtn) {
|
|
263
|
+
ui.pauseBtn.setAttribute("aria-pressed", String(state.paused));
|
|
264
|
+
ui.pauseBtn.textContent = state.paused ? "Reanudar" : "Pausar";
|
|
265
|
+
ui.pauseBtn.disabled = state.mode !== "server";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderStats() {
|
|
270
|
+
var counts = Core.statusCounts(state.built.threads);
|
|
271
|
+
if (ui.statAll) ui.statAll.textContent = String(counts.all);
|
|
272
|
+
if (ui.statPending) ui.statPending.textContent = String(counts.pending);
|
|
273
|
+
if (ui.statProgress) ui.statProgress.textContent = String(counts.accepted + counts.partial);
|
|
274
|
+
if (ui.statDone) ui.statDone.textContent = String(counts.completed);
|
|
275
|
+
if (ui.statDeclined) ui.statDeclined.textContent = String(counts.declined);
|
|
276
|
+
if (ui.countLabel) {
|
|
277
|
+
var replies = state.built.threads.reduce(function (total, thread) {
|
|
278
|
+
return total + thread.replyCount;
|
|
279
|
+
}, 0);
|
|
280
|
+
ui.countLabel.textContent =
|
|
281
|
+
counts.all + (counts.all === 1 ? " hilo" : " hilos") + " · " + replies + (replies === 1 ? " respuesta" : " respuestas") +
|
|
282
|
+
(state.built.unlinked.length ? " · " + state.built.unlinked.length + " sin hilo" : "");
|
|
283
|
+
}
|
|
284
|
+
if (ui.statusFilter) {
|
|
285
|
+
var options = [["all", "Todos"], ["pending", "Esperando"], ["accepted", "Aceptados"], ["partial", "Parciales"], ["declined", "Rechazados"], ["completed", "Completados"]];
|
|
286
|
+
options.forEach(function (pair) {
|
|
287
|
+
var option = ui.statusFilter.querySelector('option[value="' + pair[0] + '"]');
|
|
288
|
+
if (option) option.textContent = pair[1] + " (" + (counts[pair[0]] || 0) + ")";
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function syncAgentFilter() {
|
|
294
|
+
if (!ui.agentFilter) return;
|
|
295
|
+
var current = ui.agentFilter.value;
|
|
296
|
+
var agents = Object.create(null);
|
|
297
|
+
state.built.threads.forEach(function (thread) {
|
|
298
|
+
thread.participants.forEach(function (id) {
|
|
299
|
+
agents[id] = agentInfo(id).name;
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
var ids = Object.keys(agents).sort();
|
|
303
|
+
clear(ui.agentFilter);
|
|
304
|
+
appendChild(ui.agentFilter, h("option", { value: "", text: "Todos los agentes" }));
|
|
305
|
+
ids.forEach(function (id) {
|
|
306
|
+
appendChild(ui.agentFilter, h("option", { value: id, text: agents[id] + " (" + id + ")" }));
|
|
307
|
+
});
|
|
308
|
+
ui.agentFilter.value = ids.indexOf(current) !== -1 ? current : "";
|
|
309
|
+
state.filters.agent = ui.agentFilter.value;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function renderFilters() {
|
|
313
|
+
if (!ui.queryInput) return;
|
|
314
|
+
if (document.activeElement !== ui.queryInput) ui.queryInput.value = state.filters.query;
|
|
315
|
+
ui.queryInput.setAttribute("aria-label", "Buscar en hilos y respuestas");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function renderThreads() {
|
|
319
|
+
if (!ui.list) return;
|
|
320
|
+
clear(ui.list);
|
|
321
|
+
|
|
322
|
+
var visible = Core.filterThreads(state.built.threads, state.filters);
|
|
323
|
+
if (ui.countLabel) {
|
|
324
|
+
var counts = Core.statusCounts(state.built.threads);
|
|
325
|
+
var replies = state.built.threads.reduce(function (total, thread) {
|
|
326
|
+
return total + thread.replyCount;
|
|
327
|
+
}, 0);
|
|
328
|
+
ui.countLabel.textContent =
|
|
329
|
+
counts.all + (counts.all === 1 ? " hilo" : " hilos") + " · " + replies + (replies === 1 ? " respuesta" : " respuestas") +
|
|
330
|
+
(visible.length !== counts.all ? " · mostrando " + visible.length : "") +
|
|
331
|
+
(state.built.unlinked.length ? " · " + state.built.unlinked.length + " sin hilo" : "");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
ui.list.setAttribute("aria-busy", "false");
|
|
335
|
+
ui.list.hidden = visible.length === 0;
|
|
336
|
+
if (ui.empty) ui.empty.hidden = visible.length !== 0;
|
|
337
|
+
if (visible.length === 0 && ui.empty) renderEmpty();
|
|
338
|
+
|
|
339
|
+
visible.forEach(function (thread, index) {
|
|
340
|
+
appendChild(ui.list, threadNode(thread, index));
|
|
341
|
+
});
|
|
342
|
+
restoreFocus();
|
|
343
|
+
renderTimes();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function renderEmpty() {
|
|
347
|
+
clear(ui.empty);
|
|
348
|
+
var hasData = state.built.threads.length > 0;
|
|
349
|
+
appendChild(
|
|
350
|
+
ui.empty,
|
|
351
|
+
h(
|
|
352
|
+
"div",
|
|
353
|
+
{ class: "empty-card" },
|
|
354
|
+
h("h2", { text: hasData ? "Ningún hilo coincide" : "No hay pedidos de ayuda broadcast todavía" }),
|
|
355
|
+
h(
|
|
356
|
+
"p",
|
|
357
|
+
{
|
|
358
|
+
text: hasData
|
|
359
|
+
? "Ajustá el filtro o el texto de búsqueda."
|
|
360
|
+
: "El panel muestra HELP_REQUEST dirigidos a ['*'] y sus respuestas. Cuando alguno aparezca en el bus, se abre el hilo acá.",
|
|
361
|
+
},
|
|
362
|
+
),
|
|
363
|
+
h(
|
|
364
|
+
"div",
|
|
365
|
+
{ class: "empty-actions" },
|
|
366
|
+
h("button", { class: "ghost", type: "button", "data-action": "clear-filters", text: "Limpiar filtros" }),
|
|
367
|
+
state.mode === "files"
|
|
368
|
+
? h("button", { class: "ghost", type: "button", "data-action": "pick-files", text: "Cargar JSON del bus" })
|
|
369
|
+
: null,
|
|
370
|
+
h("button", { class: "ghost", type: "button", "data-action": "demo", text: "Ver datos de demo" }),
|
|
371
|
+
),
|
|
372
|
+
h("pre", { class: "empty-cmd", text: "node dashboard/server.mjs\n# → http://127.0.0.1:7788/debate.html" }),
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function renderFooter() {
|
|
378
|
+
if (!ui.footer) return;
|
|
379
|
+
clear(ui.footer);
|
|
380
|
+
var meta = state.snapshot.meta || {};
|
|
381
|
+
var parts = [];
|
|
382
|
+
if (meta.generatedAt) parts.push("snapshot " + Core.formatClock(meta.generatedAt));
|
|
383
|
+
if (meta.objective) parts.push(Core.shortText(meta.objective, 80));
|
|
384
|
+
if (Array.isArray(meta.channels) && meta.channels.length) parts.push("canales: " + meta.channels.join(", "));
|
|
385
|
+
parts.push(state.built.messages.length + " mensajes indexados");
|
|
386
|
+
appendChild(ui.footer, h("span", { text: parts.join(" · ") }));
|
|
387
|
+
|
|
388
|
+
var warnings = (state.snapshot.warnings || []).slice(0, 3);
|
|
389
|
+
if (warnings.length) {
|
|
390
|
+
appendChild(
|
|
391
|
+
ui.footer,
|
|
392
|
+
h("span", {
|
|
393
|
+
class: "warn-inline",
|
|
394
|
+
text: "⚠ " + warnings.length + " aviso(s): " + warnings.join(" · "),
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderBanner() {
|
|
401
|
+
if (!ui.banner) return;
|
|
402
|
+
clear(ui.banner);
|
|
403
|
+
var show = null;
|
|
404
|
+
if (state.mode === "demo") {
|
|
405
|
+
show = {
|
|
406
|
+
tone: "info",
|
|
407
|
+
text: "Estás viendo datos de DEMO (sintéticos). Para leer la corrida real, abrí el panel con el server local.",
|
|
408
|
+
};
|
|
409
|
+
} else if (state.mode === "files") {
|
|
410
|
+
show = {
|
|
411
|
+
tone: "info",
|
|
412
|
+
text: "Modo archivos: file:// no puede leer .oficina/. Soltá los JSON de .oficina/bus/peer/ (o corré node dashboard/server.mjs para verlo en vivo).",
|
|
413
|
+
actions: [
|
|
414
|
+
{ label: "Elegir archivos", action: "pick-files" },
|
|
415
|
+
{ label: "Ver demo", action: "demo" },
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
} else if (state.error) {
|
|
419
|
+
show = {
|
|
420
|
+
tone: "error",
|
|
421
|
+
text: "No pude leer /api/debate: " + state.error + ". Reintentando cada " + Math.round(state.backoff / 1000) + "s.",
|
|
422
|
+
actions: [
|
|
423
|
+
{ label: "Reintentar ahora", action: "reload" },
|
|
424
|
+
{ label: "Cargar JSON", action: "pick-files" },
|
|
425
|
+
],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (!show) {
|
|
429
|
+
ui.banner.hidden = true;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
ui.banner.hidden = false;
|
|
433
|
+
ui.banner.className = "banner banner-" + show.tone;
|
|
434
|
+
ui.banner.appendChild(h("p", { text: show.text }));
|
|
435
|
+
if (show.actions) {
|
|
436
|
+
var group = h("div", { class: "banner-actions" });
|
|
437
|
+
show.actions.forEach(function (item) {
|
|
438
|
+
appendChild(
|
|
439
|
+
group,
|
|
440
|
+
h("button", { class: "ghost", type: "button", "data-action": item.action, text: item.label }),
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
ui.banner.appendChild(group);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderTimes() {
|
|
448
|
+
if (!ui.list) return;
|
|
449
|
+
var nodes = ui.list.querySelectorAll("time[data-ts]");
|
|
450
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
451
|
+
nodes[i].textContent = Core.relativeTime(nodes[i].dataset.ts, Date.now());
|
|
452
|
+
}
|
|
453
|
+
if (ui.updated && state.lastUpdateAt) {
|
|
454
|
+
ui.updated.textContent =
|
|
455
|
+
"actualizado " + Core.relativeTime(new Date(state.lastUpdateAt).toISOString(), Date.now());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// -------------------------------------------------------------- hilo
|
|
460
|
+
|
|
461
|
+
function statusPill(status) {
|
|
462
|
+
return h("span", {
|
|
463
|
+
class: "pill pill-" + status,
|
|
464
|
+
title: "Estado del hilo: " + Core.statusLabel(status),
|
|
465
|
+
text: Core.statusLabel(status),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function stepsNode(thread) {
|
|
470
|
+
var replied = thread.replyCount > 0;
|
|
471
|
+
var responseLabel = replied
|
|
472
|
+
? thread.status === "declined"
|
|
473
|
+
? "Rechazado"
|
|
474
|
+
: thread.status === "partial"
|
|
475
|
+
? "Parcial"
|
|
476
|
+
: "Aceptado"
|
|
477
|
+
: "Respuesta";
|
|
478
|
+
var steps = [
|
|
479
|
+
{ label: "Pedido", done: true, active: false },
|
|
480
|
+
{
|
|
481
|
+
label: responseLabel,
|
|
482
|
+
done: replied,
|
|
483
|
+
active: !replied,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
label: "Cierre",
|
|
487
|
+
done: thread.status === "completed",
|
|
488
|
+
active: thread.status === "accepted" || thread.status === "partial" || thread.status === "declined",
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
var list = h("ol", { class: "steps", "aria-label": "Progreso del hilo" });
|
|
492
|
+
steps.forEach(function (step) {
|
|
493
|
+
var classes = "step" + (step.done ? " is-done" : "") + (step.active ? " is-active" : "");
|
|
494
|
+
var attrs = { class: classes };
|
|
495
|
+
if (step.active) attrs["aria-current"] = "step";
|
|
496
|
+
appendChild(list, h("li", attrs, h("span", { class: "dot", "aria-hidden": "true" }), step.label));
|
|
497
|
+
});
|
|
498
|
+
return list;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function threadNode(thread, index) {
|
|
502
|
+
var id = safeId(thread.id);
|
|
503
|
+
var open = isOpen(thread);
|
|
504
|
+
var root = thread.root;
|
|
505
|
+
var info = agentInfo(root.from);
|
|
506
|
+
var need = Core.describeBody(root.body).find(function (part) {
|
|
507
|
+
return part.key === "need";
|
|
508
|
+
});
|
|
509
|
+
var preview = need ? need.text : Core.bodyText(root.body);
|
|
510
|
+
var acked = Core.ackedBy(state.snapshot.acks, root.id);
|
|
511
|
+
var card = cardTitle(root.subject);
|
|
512
|
+
|
|
513
|
+
var article = h("article", {
|
|
514
|
+
class: "thread status-" + thread.status,
|
|
515
|
+
"data-thread-id": thread.id,
|
|
516
|
+
"data-fkey": "thread:" + thread.id,
|
|
517
|
+
"aria-labelledby": "h-" + id,
|
|
518
|
+
style: "--h:" + Core.agentHue(root.from),
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
var toggle = h(
|
|
522
|
+
"button",
|
|
523
|
+
{
|
|
524
|
+
class: "thread-toggle",
|
|
525
|
+
type: "button",
|
|
526
|
+
id: "h-" + id,
|
|
527
|
+
"aria-expanded": String(open),
|
|
528
|
+
"aria-controls": "b-" + id,
|
|
529
|
+
"data-action": "toggle-thread",
|
|
530
|
+
"data-fkey": "toggle:" + thread.id,
|
|
531
|
+
},
|
|
532
|
+
avatar(root.from, false),
|
|
533
|
+
h(
|
|
534
|
+
"span",
|
|
535
|
+
{ class: "thread-main" },
|
|
536
|
+
h(
|
|
537
|
+
"span",
|
|
538
|
+
{ class: "thread-title" },
|
|
539
|
+
h("span", { class: "who", text: info.name }),
|
|
540
|
+
h("span", { class: "sep", "aria-hidden": "true", text: "·" }),
|
|
541
|
+
h("span", { class: "subject", text: root.subject || "sin card" }),
|
|
542
|
+
thread.broadcast
|
|
543
|
+
? h("span", { class: "tag tag-broadcast", title: "HELP_REQUEST dirigido a todos", text: "broadcast" })
|
|
544
|
+
: h("span", { class: "tag", title: "HELP_REQUEST a varios agentes", text: "multi" }),
|
|
545
|
+
root.priority === "critical" || root.priority === "high"
|
|
546
|
+
? h("span", { class: "tag tag-" + root.priority, text: root.priority })
|
|
547
|
+
: null,
|
|
548
|
+
),
|
|
549
|
+
h("span", { class: "thread-need", text: preview || "sin detalle" }),
|
|
550
|
+
card ? h("span", { class: "thread-card", text: card }) : null,
|
|
551
|
+
),
|
|
552
|
+
h(
|
|
553
|
+
"span",
|
|
554
|
+
{ class: "thread-meta" },
|
|
555
|
+
statusPill(thread.status),
|
|
556
|
+
h("span", {
|
|
557
|
+
class: "reply-count",
|
|
558
|
+
title: thread.replyCount === 1 ? "1 respuesta" : thread.replyCount + " respuestas",
|
|
559
|
+
text: "💬 " + thread.replyCount,
|
|
560
|
+
}),
|
|
561
|
+
timeNode(root.createdAt),
|
|
562
|
+
h("span", { class: "chev", "aria-hidden": "true" }),
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
var body = h("div", {
|
|
567
|
+
class: "thread-body",
|
|
568
|
+
id: "b-" + id,
|
|
569
|
+
role: "group",
|
|
570
|
+
"aria-label": "Debate sobre " + (root.subject || "ayuda"),
|
|
571
|
+
});
|
|
572
|
+
body.hidden = !open;
|
|
573
|
+
|
|
574
|
+
appendChild(body, stepsNode(thread));
|
|
575
|
+
appendChild(body, requestNode(thread, acked));
|
|
576
|
+
appendChild(body, repliesNode(thread));
|
|
577
|
+
|
|
578
|
+
appendChild(article, h("h3", { class: "thread-heading" }, toggle));
|
|
579
|
+
appendChild(article, body);
|
|
580
|
+
article.dataset.index = String(index);
|
|
581
|
+
return article;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function requestNode(thread, acked) {
|
|
585
|
+
var root = thread.root;
|
|
586
|
+
var parts = Core.describeBody(root.body);
|
|
587
|
+
var wrap = h("section", { class: "request", "aria-label": "Pedido de ayuda" });
|
|
588
|
+
appendChild(
|
|
589
|
+
wrap,
|
|
590
|
+
h(
|
|
591
|
+
"header",
|
|
592
|
+
{ class: "request-head" },
|
|
593
|
+
avatar(root.from, true),
|
|
594
|
+
h("span", { class: "bname", text: agentInfo(root.from).name }),
|
|
595
|
+
h("span", { class: "badge badge-request", text: "pide ayuda" }),
|
|
596
|
+
timeNode(root.createdAt, "muted"),
|
|
597
|
+
h("span", { class: "muted", text: "→ " + root.to.join(", ") }),
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
if (parts.length === 0) {
|
|
601
|
+
appendChild(wrap, h("p", { class: "muted", text: "(el pedido no trae cuerpo)" }));
|
|
602
|
+
} else {
|
|
603
|
+
parts.forEach(function (part) {
|
|
604
|
+
appendChild(wrap, bodyPartNode(thread, root, part));
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
appendChild(
|
|
608
|
+
wrap,
|
|
609
|
+
h(
|
|
610
|
+
"footer",
|
|
611
|
+
{ class: "request-foot" },
|
|
612
|
+
h("span", { class: "muted", text: "ttl " + Math.round(root.ttlSec / 60) + "m" }),
|
|
613
|
+
thread.expired ? h("span", { class: "tag tag-expired", text: "vencido" }) : null,
|
|
614
|
+
h("span", { class: "muted", text: "id " + root.id }),
|
|
615
|
+
h("span", {
|
|
616
|
+
class: "muted",
|
|
617
|
+
text: acked.length ? "visto por " + acked.length + ": " + acked.map(shortAgent).join(", ") : "sin acuses",
|
|
618
|
+
}),
|
|
619
|
+
h("details", { class: "json" }, h("summary", { text: "JSON" }), h("pre", { text: JSON.stringify(root, null, 2) })),
|
|
620
|
+
),
|
|
621
|
+
);
|
|
622
|
+
return wrap;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function shortAgent(agentId) {
|
|
626
|
+
return agentInfo(agentId).name;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function bodyPartNode(thread, message, part) {
|
|
630
|
+
var key = thread.id + ":" + message.id + ":" + part.key;
|
|
631
|
+
var expanded = state.expanded.has(key);
|
|
632
|
+
var isLong = part.text.length > LONG_TEXT || part.text.indexOf("\n") !== -1;
|
|
633
|
+
var wrap = h("div", { class: "part" });
|
|
634
|
+
appendChild(wrap, h("span", { class: "part-label", text: part.label }));
|
|
635
|
+
appendChild(
|
|
636
|
+
wrap,
|
|
637
|
+
h("div", {
|
|
638
|
+
class: "part-value" + (isLong ? " is-long" : "") + (expanded ? " is-expanded" : ""),
|
|
639
|
+
text: part.text,
|
|
640
|
+
}),
|
|
641
|
+
);
|
|
642
|
+
if (isLong) {
|
|
643
|
+
appendChild(
|
|
644
|
+
wrap,
|
|
645
|
+
h("button", {
|
|
646
|
+
class: "more",
|
|
647
|
+
type: "button",
|
|
648
|
+
"data-action": "expand-body",
|
|
649
|
+
"data-key": key,
|
|
650
|
+
"data-fkey": "more:" + key,
|
|
651
|
+
"aria-expanded": String(expanded),
|
|
652
|
+
text: expanded ? "Ver menos" : "Ver completo",
|
|
653
|
+
}),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return wrap;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function repliesNode(thread) {
|
|
660
|
+
var section = h("section", { class: "replies", "aria-label": "Respuestas al pedido" });
|
|
661
|
+
appendChild(
|
|
662
|
+
section,
|
|
663
|
+
h("h4", {
|
|
664
|
+
class: "replies-title",
|
|
665
|
+
text: thread.replyCount === 1 ? "1 respuesta" : thread.replyCount + " respuestas",
|
|
666
|
+
}),
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
if (thread.replyCount === 0) {
|
|
670
|
+
appendChild(
|
|
671
|
+
section,
|
|
672
|
+
h("p", {
|
|
673
|
+
class: "muted empty-replies",
|
|
674
|
+
text: "Todavía nadie contestó. " +
|
|
675
|
+
(thread.expired ? "El pedido ya venció por TTL." : "Vence " + Core.relativeTime(new Date(thread.root.expiresAtMs).toISOString(), state.lastUpdateAt || Date.now()) + "."),
|
|
676
|
+
}),
|
|
677
|
+
);
|
|
678
|
+
return section;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
var list = h("ol", { class: "bubbles" });
|
|
682
|
+
thread.replies.forEach(function (message) {
|
|
683
|
+
appendChild(list, bubbleNode(thread, message));
|
|
684
|
+
});
|
|
685
|
+
appendChild(section, list);
|
|
686
|
+
return section;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function bubbleNode(thread, message) {
|
|
690
|
+
var info = agentInfo(message.from);
|
|
691
|
+
var parts = Core.describeBody(message.body);
|
|
692
|
+
var bubble = h("li", {
|
|
693
|
+
class: "bubble kind-" + message.kind.toLowerCase().replace(/_/g, "-"),
|
|
694
|
+
"data-message-id": message.id,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
appendChild(bubble, avatar(message.from, true));
|
|
698
|
+
var content = h("div", { class: "bubble-content" });
|
|
699
|
+
appendChild(
|
|
700
|
+
content,
|
|
701
|
+
h(
|
|
702
|
+
"header",
|
|
703
|
+
{ class: "bubble-head" },
|
|
704
|
+
h("span", { class: "bname", text: info.name }),
|
|
705
|
+
h("span", { class: "badge badge-" + message.kind.toLowerCase().replace(/_/g, "-"), text: Core.kindLabel(message.kind) }),
|
|
706
|
+
message.priority === "critical" || message.priority === "high"
|
|
707
|
+
? h("span", { class: "tag tag-" + message.priority, text: message.priority })
|
|
708
|
+
: null,
|
|
709
|
+
timeNode(message.createdAt, "muted"),
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
if (parts.length === 0) {
|
|
714
|
+
appendChild(content, h("p", { class: "muted", text: "(respuesta sin cuerpo)" }));
|
|
715
|
+
} else {
|
|
716
|
+
parts.forEach(function (part) {
|
|
717
|
+
appendChild(content, bodyPartNode(thread, message, part));
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
appendChild(
|
|
722
|
+
content,
|
|
723
|
+
h(
|
|
724
|
+
"footer",
|
|
725
|
+
{ class: "bubble-foot" },
|
|
726
|
+
h("span", { class: "muted", text: "→ " + message.to.join(", ") }),
|
|
727
|
+
message.reattached ? h("span", { class: "tag tag-reattached", title: "inReplyTo apuntaba a un mensaje vencido; se enganchó por subject", text: "reenganchado" }) : null,
|
|
728
|
+
h("span", { class: "muted", text: Core.formatClock(message.createdAt) }),
|
|
729
|
+
h("span", { class: "muted", text: "id " + message.id }),
|
|
730
|
+
h("details", { class: "json" }, h("summary", { text: "JSON" }), h("pre", { text: JSON.stringify(message, null, 2) })),
|
|
731
|
+
),
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
appendChild(bubble, content);
|
|
735
|
+
return bubble;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ------------------------------------------------------- foco y estado
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Recuerda el foco sólo si está dentro de la lista de hilos: es lo único que
|
|
742
|
+
* se recrea en cada render. Si el foco está en el buscador, no se toca.
|
|
743
|
+
*/
|
|
744
|
+
function rememberFocus() {
|
|
745
|
+
var active = document.activeElement;
|
|
746
|
+
var inside = !!(active && ui.list && ui.list.contains(active));
|
|
747
|
+
state.focusKey = inside && active.dataset && active.dataset.fkey ? active.dataset.fkey : null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function restoreFocus() {
|
|
751
|
+
if (!state.focusKey) return;
|
|
752
|
+
var node = ui.list.querySelector('[data-fkey="' + cssEscape(state.focusKey) + '"]');
|
|
753
|
+
if (node && typeof node.focus === "function") node.focus({ preventScroll: true });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function cssEscape(value) {
|
|
757
|
+
if (root.CSS && typeof root.CSS.escape === "function") return root.CSS.escape(value);
|
|
758
|
+
return String(value).replace(/["\\]/g, "\\$&");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function toggleThread(threadId, forceOpen) {
|
|
762
|
+
var thread = findThread(threadId);
|
|
763
|
+
if (!thread) return;
|
|
764
|
+
var open = forceOpen === undefined ? !isOpen(thread) : forceOpen;
|
|
765
|
+
if (open) {
|
|
766
|
+
state.closed.delete(threadId);
|
|
767
|
+
state.open.add(threadId);
|
|
768
|
+
} else {
|
|
769
|
+
state.open.delete(threadId);
|
|
770
|
+
state.closed.add(threadId);
|
|
771
|
+
}
|
|
772
|
+
var body = document.getElementById("b-" + safeId(threadId));
|
|
773
|
+
var toggle = document.getElementById("h-" + safeId(threadId));
|
|
774
|
+
if (body) body.hidden = !open;
|
|
775
|
+
if (toggle) toggle.setAttribute("aria-expanded", String(open));
|
|
776
|
+
/* Sin announce: aria-expanded ya lo comunica el lector de pantalla. */
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function findThread(threadId) {
|
|
780
|
+
for (var i = 0; i < state.built.threads.length; i += 1) {
|
|
781
|
+
if (state.built.threads[i].id === threadId) return state.built.threads[i];
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function setAllOpen(open) {
|
|
787
|
+
state.built.threads.forEach(function (thread) {
|
|
788
|
+
if (open) {
|
|
789
|
+
state.closed.delete(thread.id);
|
|
790
|
+
state.open.add(thread.id);
|
|
791
|
+
} else {
|
|
792
|
+
state.open.delete(thread.id);
|
|
793
|
+
state.closed.add(thread.id);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
render();
|
|
797
|
+
announce(open ? "Todos los hilos abiertos." : "Todos los hilos cerrados.");
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function toggleBodyKey(key, button) {
|
|
801
|
+
var expanded = state.expanded.has(key);
|
|
802
|
+
if (expanded) state.expanded.delete(key);
|
|
803
|
+
else state.expanded.add(key);
|
|
804
|
+
var value = button.parentNode ? button.parentNode.querySelector(".part-value") : null;
|
|
805
|
+
if (value) value.classList.toggle("is-expanded", !expanded);
|
|
806
|
+
button.setAttribute("aria-expanded", String(!expanded));
|
|
807
|
+
button.textContent = expanded ? "Ver completo" : "Ver menos";
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// --------------------------------------------------------- carga de datos
|
|
811
|
+
|
|
812
|
+
function schedule(delay) {
|
|
813
|
+
if (state.timer) clearTimeout(state.timer);
|
|
814
|
+
if (state.mode !== "server" && state.mode !== "snapshot") return;
|
|
815
|
+
state.timer = setTimeout(function () {
|
|
816
|
+
poll(false);
|
|
817
|
+
}, delay == null ? POLL_MS : delay);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function poll(force) {
|
|
821
|
+
if (state.mode !== "server" && state.mode !== "snapshot") return;
|
|
822
|
+
if (!force && (state.paused || document.hidden)) {
|
|
823
|
+
schedule(POLL_MS);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
ensureBusy(true);
|
|
827
|
+
var request = state.mode === "server" ? Data.loadFromServer(state.apiBase) : Data.loadFromUrl(state.srcUrl);
|
|
828
|
+
request
|
|
829
|
+
.then(function (snapshot) {
|
|
830
|
+
state.backoff = POLL_MS;
|
|
831
|
+
applySnapshot(snapshot);
|
|
832
|
+
})
|
|
833
|
+
.catch(function (error) {
|
|
834
|
+
state.error = String(error && error.message ? error.message : error);
|
|
835
|
+
state.backoff = Math.min(state.backoff * 2, MAX_BACKOFF_MS);
|
|
836
|
+
renderConnection();
|
|
837
|
+
renderBanner();
|
|
838
|
+
})
|
|
839
|
+
.finally(function () {
|
|
840
|
+
ensureBusy(false);
|
|
841
|
+
schedule(state.backoff);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function ensureBusy(busy) {
|
|
846
|
+
if (ui.list) ui.list.setAttribute("aria-busy", busy ? "true" : "false");
|
|
847
|
+
if (ui.conn) ui.conn.classList.toggle("is-loading", busy);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function loadDemo() {
|
|
851
|
+
stopPolling();
|
|
852
|
+
state.mode = "demo";
|
|
853
|
+
var snapshot = Core.readSnapshot(root.DebateDemo ? root.DebateDemo() : { messages: [] });
|
|
854
|
+
snapshot.meta.source = "demo";
|
|
855
|
+
snapshot.meta.demo = true;
|
|
856
|
+
state.loadedOnce = true; // no anunciar el pasado como si fuera nuevo
|
|
857
|
+
applySnapshot(snapshot, { notice: "Datos de demo cargados (sintéticos)." });
|
|
858
|
+
renderBanner();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function loadFiles(fileList) {
|
|
862
|
+
return Data.readFiles(fileList).then(function (result) {
|
|
863
|
+
if (result.fileCount === 0) return;
|
|
864
|
+
stopPolling();
|
|
865
|
+
state.mode = "files";
|
|
866
|
+
var snapshot = result.snapshot;
|
|
867
|
+
snapshot.meta.source = "files";
|
|
868
|
+
applySnapshot(snapshot, {
|
|
869
|
+
notice:
|
|
870
|
+
"Cargados " + result.fileCount + " archivo(s): " + snapshot.messages.length + " mensajes" +
|
|
871
|
+
(result.errors.length ? ", " + result.errors.length + " con error." : "."),
|
|
872
|
+
});
|
|
873
|
+
if (result.errors.length) {
|
|
874
|
+
state.error = result.errors.map(function (entry) {
|
|
875
|
+
return entry.name + ": " + entry.error;
|
|
876
|
+
}).join(" · ");
|
|
877
|
+
renderBanner();
|
|
878
|
+
}
|
|
879
|
+
renderBanner();
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function stopPolling() {
|
|
884
|
+
if (state.timer) clearTimeout(state.timer);
|
|
885
|
+
state.timer = null;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ------------------------------------------------------------ interacción
|
|
889
|
+
|
|
890
|
+
function bindEvents() {
|
|
891
|
+
ui.list.addEventListener("click", function (event) {
|
|
892
|
+
var action = event.target.closest ? event.target.closest("[data-action]") : null;
|
|
893
|
+
if (!action) return;
|
|
894
|
+
var name = action.dataset.action;
|
|
895
|
+
if (name === "toggle-thread") {
|
|
896
|
+
event.preventDefault();
|
|
897
|
+
toggleThread(action.closest("[data-thread-id]").dataset.threadId);
|
|
898
|
+
} else if (name === "expand-body") {
|
|
899
|
+
toggleBodyKey(action.dataset.key, action);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
document.addEventListener("click", function (event) {
|
|
904
|
+
var action = event.target.closest ? event.target.closest("[data-action]") : null;
|
|
905
|
+
if (!action) return;
|
|
906
|
+
var name = action.dataset.action;
|
|
907
|
+
if (name === "reload") poll(true);
|
|
908
|
+
else if (name === "pause") {
|
|
909
|
+
state.paused = !state.paused;
|
|
910
|
+
renderConnection();
|
|
911
|
+
announce(state.paused ? "Actualización pausada." : "Actualización reanudada.");
|
|
912
|
+
} else if (name === "expand-all") setAllOpen(true);
|
|
913
|
+
else if (name === "collapse-all") setAllOpen(false);
|
|
914
|
+
else if (name === "theme") toggleTheme();
|
|
915
|
+
else if (name === "help") openHelp(true);
|
|
916
|
+
else if (name === "close-help") openHelp(false);
|
|
917
|
+
else if (name === "demo") loadDemo();
|
|
918
|
+
else if (name === "pick-files") ui.fileInput.click();
|
|
919
|
+
else if (name === "clear-filters") {
|
|
920
|
+
state.filters = { status: "all", query: "", agent: "" };
|
|
921
|
+
ui.statusFilter.value = "all";
|
|
922
|
+
ui.agentFilter.value = "";
|
|
923
|
+
ui.queryInput.value = "";
|
|
924
|
+
renderThreads();
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
ui.queryInput.addEventListener("input", debounce(function () {
|
|
929
|
+
state.filters.query = ui.queryInput.value;
|
|
930
|
+
renderThreads();
|
|
931
|
+
}, 140));
|
|
932
|
+
|
|
933
|
+
ui.statusFilter.addEventListener("change", function () {
|
|
934
|
+
state.filters.status = ui.statusFilter.value;
|
|
935
|
+
renderThreads();
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
ui.agentFilter.addEventListener("change", function () {
|
|
939
|
+
state.filters.agent = ui.agentFilter.value;
|
|
940
|
+
renderThreads();
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
ui.fileInput.addEventListener("change", function () {
|
|
944
|
+
loadFiles(ui.fileInput.files);
|
|
945
|
+
ui.fileInput.value = "";
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Navegación con flechas entre cabeceras de hilo.
|
|
949
|
+
ui.list.addEventListener("keydown", function (event) {
|
|
950
|
+
var target = event.target;
|
|
951
|
+
if (!target.classList || !target.classList.contains("thread-toggle")) return;
|
|
952
|
+
var toggles = Array.prototype.slice.call(ui.list.querySelectorAll(".thread-toggle"));
|
|
953
|
+
var index = toggles.indexOf(target);
|
|
954
|
+
var next = -1;
|
|
955
|
+
if (event.key === "ArrowDown") next = Math.min(index + 1, toggles.length - 1);
|
|
956
|
+
else if (event.key === "ArrowUp") next = Math.max(index - 1, 0);
|
|
957
|
+
else if (event.key === "Home") next = 0;
|
|
958
|
+
else if (event.key === "End") next = toggles.length - 1;
|
|
959
|
+
if (next >= 0 && toggles[next] && next !== index) {
|
|
960
|
+
event.preventDefault();
|
|
961
|
+
toggles[next].focus();
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
document.addEventListener("keydown", function (event) {
|
|
966
|
+
var target = event.target;
|
|
967
|
+
var tag = (target.tagName || "").toLowerCase();
|
|
968
|
+
var typing = tag === "input" || tag === "select" || tag === "textarea" || target.isContentEditable;
|
|
969
|
+
if (event.key === "Escape") {
|
|
970
|
+
if (ui.help.open) {
|
|
971
|
+
openHelp(false);
|
|
972
|
+
event.preventDefault();
|
|
973
|
+
} else if (typing) {
|
|
974
|
+
target.blur();
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (event.key === "/" && !typing) {
|
|
979
|
+
event.preventDefault();
|
|
980
|
+
ui.queryInput.focus();
|
|
981
|
+
ui.queryInput.select();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (typing || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
985
|
+
if (event.key === "?" ) {
|
|
986
|
+
event.preventDefault();
|
|
987
|
+
openHelp(!ui.help.open);
|
|
988
|
+
} else if (event.key === "r" || event.key === "R") {
|
|
989
|
+
poll(true);
|
|
990
|
+
} else if (event.key === "p" || event.key === "P") {
|
|
991
|
+
state.paused = !state.paused;
|
|
992
|
+
renderConnection();
|
|
993
|
+
announce(state.paused ? "Actualización pausada." : "Actualización reanudada.");
|
|
994
|
+
} else if (event.key === "e" || event.key === "E") {
|
|
995
|
+
setAllOpen(true);
|
|
996
|
+
} else if (event.key === "x" || event.key === "X") {
|
|
997
|
+
setAllOpen(false);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Drag & drop de JSON del bus.
|
|
1002
|
+
["dragenter", "dragover"].forEach(function (name) {
|
|
1003
|
+
document.addEventListener(name, function (event) {
|
|
1004
|
+
if (!event.dataTransfer || Array.prototype.indexOf.call(event.dataTransfer.types || [], "Files") === -1) return;
|
|
1005
|
+
event.preventDefault();
|
|
1006
|
+
ui.dropzone.hidden = false;
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
["dragleave", "drop"].forEach(function (name) {
|
|
1010
|
+
document.addEventListener(name, function (event) {
|
|
1011
|
+
if (name === "dragleave" && event.relatedTarget) return;
|
|
1012
|
+
if (ui.dropzone) ui.dropzone.hidden = true;
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
document.addEventListener("drop", function (event) {
|
|
1016
|
+
if (!event.dataTransfer || !event.dataTransfer.files || event.dataTransfer.files.length === 0) return;
|
|
1017
|
+
event.preventDefault();
|
|
1018
|
+
loadFiles(event.dataTransfer.files);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
document.addEventListener("visibilitychange", function () {
|
|
1022
|
+
if (!document.hidden) poll(true);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function openHelp(open) {
|
|
1027
|
+
if (!ui.help) return;
|
|
1028
|
+
if (open) {
|
|
1029
|
+
if (typeof ui.help.showModal === "function") ui.help.showModal();
|
|
1030
|
+
else ui.help.setAttribute("open", "");
|
|
1031
|
+
} else if (typeof ui.help.close === "function") {
|
|
1032
|
+
ui.help.close();
|
|
1033
|
+
} else {
|
|
1034
|
+
ui.help.removeAttribute("open");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function debounce(fn, wait) {
|
|
1039
|
+
var timer = null;
|
|
1040
|
+
return function () {
|
|
1041
|
+
if (timer) clearTimeout(timer);
|
|
1042
|
+
timer = setTimeout(fn, wait);
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ----------------------------------------------------------------- tema
|
|
1047
|
+
|
|
1048
|
+
function currentTheme() {
|
|
1049
|
+
return document.documentElement.getAttribute("data-theme") || "dark";
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function toggleTheme() {
|
|
1053
|
+
var next = currentTheme() === "dark" ? "light" : "dark";
|
|
1054
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
1055
|
+
try {
|
|
1056
|
+
localStorage.setItem("oficina.theme", next);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
/* modo privado / file:// sin storage */
|
|
1059
|
+
}
|
|
1060
|
+
announce("Tema " + (next === "dark" ? "oscuro" : "claro") + ".");
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function initTheme() {
|
|
1064
|
+
var stored = null;
|
|
1065
|
+
try {
|
|
1066
|
+
stored = localStorage.getItem("oficina.theme");
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
stored = null;
|
|
1069
|
+
}
|
|
1070
|
+
var preferred = stored || (root.matchMedia && root.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
|
1071
|
+
document.documentElement.setAttribute("data-theme", preferred);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// ---------------------------------------------------------------- boot
|
|
1075
|
+
|
|
1076
|
+
function grabUi() {
|
|
1077
|
+
ui = {
|
|
1078
|
+
list: document.getElementById("threads"),
|
|
1079
|
+
empty: document.getElementById("empty"),
|
|
1080
|
+
banner: document.getElementById("banner"),
|
|
1081
|
+
live: document.getElementById("live"),
|
|
1082
|
+
conn: document.getElementById("conn"),
|
|
1083
|
+
updated: document.getElementById("updated"),
|
|
1084
|
+
countLabel: document.getElementById("count-label"),
|
|
1085
|
+
statAll: document.getElementById("stat-all"),
|
|
1086
|
+
statPending: document.getElementById("stat-pending"),
|
|
1087
|
+
statProgress: document.getElementById("stat-progress"),
|
|
1088
|
+
statDone: document.getElementById("stat-done"),
|
|
1089
|
+
statDeclined: document.getElementById("stat-declined"),
|
|
1090
|
+
queryInput: document.getElementById("filter-q"),
|
|
1091
|
+
statusFilter: document.getElementById("filter-status"),
|
|
1092
|
+
agentFilter: document.getElementById("filter-agent"),
|
|
1093
|
+
fileInput: document.getElementById("file-input"),
|
|
1094
|
+
footer: document.getElementById("footer-meta"),
|
|
1095
|
+
help: document.getElementById("help"),
|
|
1096
|
+
dropzone: document.getElementById("dropzone"),
|
|
1097
|
+
pauseBtn: document.querySelector('[data-action="pause"]'),
|
|
1098
|
+
};
|
|
1099
|
+
state.paused = false;
|
|
1100
|
+
return Boolean(ui.list && ui.live);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function boot() {
|
|
1104
|
+
if (!document.getElementById("threads")) return;
|
|
1105
|
+
if (!grabUi()) return;
|
|
1106
|
+
initTheme();
|
|
1107
|
+
bindEvents();
|
|
1108
|
+
|
|
1109
|
+
// Guardar el foco antes de cada re-render (los nodos se recrean).
|
|
1110
|
+
document.addEventListener("focusin", rememberFocus);
|
|
1111
|
+
|
|
1112
|
+
var detected = Data.detectMode(root.location);
|
|
1113
|
+
state.mode = detected.mode;
|
|
1114
|
+
state.apiBase = detected.apiBase || "";
|
|
1115
|
+
state.srcUrl = detected.srcUrl;
|
|
1116
|
+
state.reason = detected.reason;
|
|
1117
|
+
|
|
1118
|
+
render();
|
|
1119
|
+
renderEmpty();
|
|
1120
|
+
|
|
1121
|
+
if (state.mode === "demo") {
|
|
1122
|
+
loadDemo();
|
|
1123
|
+
} else if (state.mode === "files") {
|
|
1124
|
+
state.mode = "server"; // probamos igual: algunos navegadores permiten file://+fetch
|
|
1125
|
+
poll(true);
|
|
1126
|
+
setTimeout(function () {
|
|
1127
|
+
if (state.error) {
|
|
1128
|
+
stopPolling();
|
|
1129
|
+
state.mode = "files";
|
|
1130
|
+
state.error = null;
|
|
1131
|
+
render();
|
|
1132
|
+
renderBanner();
|
|
1133
|
+
announce("Modo archivos: soltá los JSON del bus para ver los hilos.");
|
|
1134
|
+
}
|
|
1135
|
+
}, 1500);
|
|
1136
|
+
} else {
|
|
1137
|
+
poll(true);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Refresco de "hace X" sin re-renderizar estructuras.
|
|
1141
|
+
setInterval(renderTimes, 10000);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (document.readyState === "loading") {
|
|
1145
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
1146
|
+
} else {
|
|
1147
|
+
boot();
|
|
1148
|
+
}
|
|
1149
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, document);
|