@bonginkan/maria-lite 6.2.0 → 6.2.2
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 +29 -2
- package/dist/cli.cjs +267430 -0
- package/dist/desktop-client.js +13289 -0
- package/dist/ext.cjs +99686 -0
- package/dist/gui-client.js +2210 -0
- package/origin/index.meta.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,2210 @@
|
|
|
1
|
+
// services/gui-lite/client/gui-client.ts
|
|
2
|
+
var $ = (id) => {
|
|
3
|
+
const el = document.getElementById(id);
|
|
4
|
+
if (!el) throw new Error(`Missing element: #${id}`);
|
|
5
|
+
return el;
|
|
6
|
+
};
|
|
7
|
+
var SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
8
|
+
var state = {
|
|
9
|
+
history: [],
|
|
10
|
+
histIdx: -1,
|
|
11
|
+
jobs: [],
|
|
12
|
+
runs: [],
|
|
13
|
+
runFilesById: {},
|
|
14
|
+
mdCache: /* @__PURE__ */ new Map(),
|
|
15
|
+
multiverse: [],
|
|
16
|
+
mvLegend: null,
|
|
17
|
+
kp: [],
|
|
18
|
+
kpNdcMeta: null,
|
|
19
|
+
kpLegend: null,
|
|
20
|
+
kpLeafItems: /* @__PURE__ */ new Map(),
|
|
21
|
+
ui: {
|
|
22
|
+
selectedRunId: null,
|
|
23
|
+
detailLoading: false,
|
|
24
|
+
lastDetailRunId: null,
|
|
25
|
+
quitting: false,
|
|
26
|
+
spinTimer: null,
|
|
27
|
+
spinIndex: 0,
|
|
28
|
+
spinText: "",
|
|
29
|
+
refreshTimer: null,
|
|
30
|
+
lastRunsSig: "",
|
|
31
|
+
showAllArtifactsByRunId: {}
|
|
32
|
+
},
|
|
33
|
+
avatar: { has: false },
|
|
34
|
+
uni: {
|
|
35
|
+
spec: null,
|
|
36
|
+
contexts: null,
|
|
37
|
+
nodes: [],
|
|
38
|
+
edges: [],
|
|
39
|
+
seek: 0,
|
|
40
|
+
playing: false,
|
|
41
|
+
timer: null,
|
|
42
|
+
pos: /* @__PURE__ */ new Map(),
|
|
43
|
+
hit: [],
|
|
44
|
+
selectedNodeId: null,
|
|
45
|
+
flowMeta: null,
|
|
46
|
+
view: { scale: 1, tx: 0, ty: 0, panning: false, panX: 0, panY: 0, pinchDist: 0, pinchScale: 1 }
|
|
47
|
+
},
|
|
48
|
+
nav: {
|
|
49
|
+
mvOpenMajor: /* @__PURE__ */ new Set(),
|
|
50
|
+
mvOpenCode: /* @__PURE__ */ new Set(),
|
|
51
|
+
kpOpenMajor: /* @__PURE__ */ new Set(),
|
|
52
|
+
kpOpenCode: /* @__PURE__ */ new Set()
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function setTab(tab) {
|
|
56
|
+
document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.tab === tab));
|
|
57
|
+
document.querySelectorAll(".view").forEach((v2) => v2.classList.remove("active"));
|
|
58
|
+
const v = document.getElementById("view-" + tab);
|
|
59
|
+
if (v) v.classList.add("active");
|
|
60
|
+
}
|
|
61
|
+
function decodeFileUrlToPath(input) {
|
|
62
|
+
const s = String(input || "").trim();
|
|
63
|
+
if (!s) return "";
|
|
64
|
+
if (!s.startsWith("file://")) return s;
|
|
65
|
+
try {
|
|
66
|
+
const u = new URL(s);
|
|
67
|
+
let p = u.pathname || "";
|
|
68
|
+
try {
|
|
69
|
+
p = decodeURIComponent(p);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
if (/^\/[A-Za-z]:\//.test(p)) p = p.slice(1);
|
|
73
|
+
return p;
|
|
74
|
+
} catch {
|
|
75
|
+
const m = /^file:\/\/\/(.*)$/i.exec(s);
|
|
76
|
+
const raw = m?.[1] ? `/${m[1]}` : s;
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(raw);
|
|
79
|
+
} catch {
|
|
80
|
+
return raw;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function quoteForCli(s) {
|
|
85
|
+
const t = String(s || "").trim();
|
|
86
|
+
if (!t) return "";
|
|
87
|
+
return JSON.stringify(t);
|
|
88
|
+
}
|
|
89
|
+
function applyDroppedTextToInput(text, target) {
|
|
90
|
+
const raw = String(text || "").trim();
|
|
91
|
+
if (!raw) return;
|
|
92
|
+
const el = target || document.activeElement;
|
|
93
|
+
const input = el && el.tagName && (String(el.tagName).toLowerCase() === "input" || String(el.tagName).toLowerCase() === "textarea") ? el : null;
|
|
94
|
+
const fallback = document.getElementById("cmdInput");
|
|
95
|
+
const dst = input || fallback;
|
|
96
|
+
if (!dst) return;
|
|
97
|
+
const cur = String(dst.value || "");
|
|
98
|
+
const sep = cur && !cur.endsWith(" ") ? " " : "";
|
|
99
|
+
dst.value = cur + sep + raw;
|
|
100
|
+
try {
|
|
101
|
+
dst.focus();
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function attachDragAndDropPath() {
|
|
106
|
+
const prevent = (e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener("dragover", prevent);
|
|
112
|
+
window.addEventListener("drop", (e) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
const dt = e.dataTransfer;
|
|
116
|
+
if (!dt) return;
|
|
117
|
+
const uriList = String(dt.getData("text/uri-list") || "").trim();
|
|
118
|
+
if (uriList) {
|
|
119
|
+
const first = uriList.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"))[0];
|
|
120
|
+
if (first) {
|
|
121
|
+
const p = decodeFileUrlToPath(first);
|
|
122
|
+
applyDroppedTextToInput(quoteForCli(p), e.target);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const plain = String(dt.getData("text/plain") || "").trim();
|
|
127
|
+
if (plain) {
|
|
128
|
+
applyDroppedTextToInput(quoteForCli(decodeFileUrlToPath(plain)), e.target);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const files = Array.from(dt.files || []);
|
|
132
|
+
if (files.length) {
|
|
133
|
+
const f = files[0];
|
|
134
|
+
(async () => {
|
|
135
|
+
try {
|
|
136
|
+
const buf = await f.arrayBuffer();
|
|
137
|
+
const bytesBase64 = arrayBufferToBase64(buf);
|
|
138
|
+
const out = await api("/api/drop-file", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: { "content-type": "application/json" },
|
|
141
|
+
body: JSON.stringify({ filename: f.name || "drop.bin", mime: f.type || "application/octet-stream", bytesBase64 })
|
|
142
|
+
});
|
|
143
|
+
const abs = out && typeof out.absPath === "string" ? String(out.absPath).trim() : "";
|
|
144
|
+
if (abs) {
|
|
145
|
+
applyDroppedTextToInput(quoteForCli(abs), e.target);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
const name = String(f.webkitRelativePath || f.name || "").trim();
|
|
151
|
+
if (name) applyDroppedTextToInput(quoteForCli(name), e.target);
|
|
152
|
+
})();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function applyTheme(theme, opts) {
|
|
157
|
+
const t = theme === "terminal" ? "terminal" : "light";
|
|
158
|
+
document.documentElement.dataset.theme = t;
|
|
159
|
+
try {
|
|
160
|
+
localStorage.setItem("maria.gui.theme", t);
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
const persist = opts?.persist !== false;
|
|
164
|
+
if (persist) {
|
|
165
|
+
api("/api/gui/state", {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: { "content-type": "application/json" },
|
|
168
|
+
body: JSON.stringify({ state: { theme: t } })
|
|
169
|
+
}).catch(() => {
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
try {
|
|
175
|
+
updateUniverseReplay();
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function initThemeFromServer() {
|
|
183
|
+
let saved = "";
|
|
184
|
+
try {
|
|
185
|
+
const r = await api("/api/gui/state");
|
|
186
|
+
const st = r && typeof r === "object" ? r.state : null;
|
|
187
|
+
saved = st && typeof st.theme === "string" ? String(st.theme).trim() : "";
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
if (!saved) {
|
|
191
|
+
try {
|
|
192
|
+
saved = String(localStorage.getItem("maria.gui.theme") || "");
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
applyTheme(saved || "light", { persist: false });
|
|
197
|
+
const sel = document.getElementById("themeSel");
|
|
198
|
+
if (sel) sel.value = saved === "terminal" ? "terminal" : "light";
|
|
199
|
+
}
|
|
200
|
+
function readFlowLayoutDir() {
|
|
201
|
+
const sel = document.getElementById("flowDirSel");
|
|
202
|
+
const vDom = sel ? String(sel.value || "").trim() : "";
|
|
203
|
+
if (vDom === "vertical" || vDom === "horizontal") return vDom;
|
|
204
|
+
let saved = "";
|
|
205
|
+
try {
|
|
206
|
+
saved = String(localStorage.getItem("maria.gui.flowDir") || "");
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
return saved === "vertical" ? "vertical" : "horizontal";
|
|
210
|
+
}
|
|
211
|
+
function readFlowParallelEnabled() {
|
|
212
|
+
const sel = document.getElementById("flowParallelSel");
|
|
213
|
+
const vDom = sel ? String(sel.value || "").trim() : "";
|
|
214
|
+
if (vDom === "off") return false;
|
|
215
|
+
if (vDom === "on") return true;
|
|
216
|
+
let saved = "";
|
|
217
|
+
try {
|
|
218
|
+
saved = String(localStorage.getItem("maria.gui.flowParallel") || "");
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
return saved !== "off";
|
|
222
|
+
}
|
|
223
|
+
async function initFlowViewOptionsFromServer() {
|
|
224
|
+
let dir = "horizontal";
|
|
225
|
+
let par = "on";
|
|
226
|
+
try {
|
|
227
|
+
const r = await api("/api/gui/state");
|
|
228
|
+
const st = r && typeof r === "object" ? r.state : null;
|
|
229
|
+
const d = st && typeof st.flowDir === "string" ? String(st.flowDir).trim() : "";
|
|
230
|
+
const p = st && typeof st.flowParallel === "string" ? String(st.flowParallel).trim() : "";
|
|
231
|
+
dir = d === "vertical" ? "vertical" : "horizontal";
|
|
232
|
+
par = p === "off" ? "off" : "on";
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
const dirSel = document.getElementById("flowDirSel");
|
|
236
|
+
if (dirSel) dirSel.value = dir;
|
|
237
|
+
const parSel = document.getElementById("flowParallelSel");
|
|
238
|
+
if (parSel) parSel.value = par;
|
|
239
|
+
}
|
|
240
|
+
async function api(path, opts) {
|
|
241
|
+
const res = await fetch(path, opts);
|
|
242
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
243
|
+
const ct = res.headers.get("content-type") || "";
|
|
244
|
+
if (ct.includes("application/json")) return await res.json();
|
|
245
|
+
return await res.text();
|
|
246
|
+
}
|
|
247
|
+
function escapeHtml(s) {
|
|
248
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
249
|
+
}
|
|
250
|
+
function decodeVisibleNewlines(s) {
|
|
251
|
+
return String(s ?? "").replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/¥n/g, "\n").replace(/\\t/g, " ");
|
|
252
|
+
}
|
|
253
|
+
function mdToHtml(src) {
|
|
254
|
+
const s = String(src ?? "");
|
|
255
|
+
const esc = (t) => escapeHtml(t);
|
|
256
|
+
const lines = s.split(/\r?\n/);
|
|
257
|
+
const out = [];
|
|
258
|
+
let inCode = false;
|
|
259
|
+
let buf = [];
|
|
260
|
+
const flushPara = (p) => {
|
|
261
|
+
if (!p.length) return;
|
|
262
|
+
out.push("<p>" + p.join(" ") + "</p>");
|
|
263
|
+
p.length = 0;
|
|
264
|
+
};
|
|
265
|
+
let para = [];
|
|
266
|
+
let inUl = false;
|
|
267
|
+
for (const l0 of lines) {
|
|
268
|
+
const l = String(l0 ?? "");
|
|
269
|
+
if (/^```/.test(l)) {
|
|
270
|
+
if (inCode) {
|
|
271
|
+
out.push("<pre><code>" + esc(buf.join("\n")) + "</code></pre>");
|
|
272
|
+
buf = [];
|
|
273
|
+
inCode = false;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
flushPara(para);
|
|
277
|
+
if (inUl) {
|
|
278
|
+
out.push("</ul>");
|
|
279
|
+
inUl = false;
|
|
280
|
+
}
|
|
281
|
+
inCode = true;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (inCode) {
|
|
285
|
+
buf.push(l);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const h = /^(#{1,3})\s+(.*)$/.exec(l);
|
|
289
|
+
if (h) {
|
|
290
|
+
flushPara(para);
|
|
291
|
+
if (inUl) {
|
|
292
|
+
out.push("</ul>");
|
|
293
|
+
inUl = false;
|
|
294
|
+
}
|
|
295
|
+
const lvl = h[1].length;
|
|
296
|
+
out.push(`<h${lvl}>${esc(h[2])}</h${lvl}>`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const li = /^\s*[-*]\s+(.+)$/.exec(l);
|
|
300
|
+
if (li) {
|
|
301
|
+
flushPara(para);
|
|
302
|
+
if (!inUl) {
|
|
303
|
+
out.push("<ul>");
|
|
304
|
+
inUl = true;
|
|
305
|
+
}
|
|
306
|
+
out.push("<li>" + esc(li[1]).replace(/`([^`]+)`/g, "<code>$1</code>") + "</li>");
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (inUl && !l.trim()) {
|
|
310
|
+
out.push("</ul>");
|
|
311
|
+
inUl = false;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!l.trim()) {
|
|
315
|
+
flushPara(para);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
para.push(esc(l).replace(/`([^`]+)`/g, "<code>$1</code>"));
|
|
319
|
+
}
|
|
320
|
+
if (inCode) out.push("<pre><code>" + esc(buf.join("\n")) + "</code></pre>");
|
|
321
|
+
if (inUl) out.push("</ul>");
|
|
322
|
+
flushPara(para);
|
|
323
|
+
return out.join("\n");
|
|
324
|
+
}
|
|
325
|
+
function setSpinner(text) {
|
|
326
|
+
state.ui.spinText = String(text || "");
|
|
327
|
+
if (state.ui.spinTimer != null) return;
|
|
328
|
+
state.ui.spinTimer = window.setInterval(() => {
|
|
329
|
+
state.ui.spinIndex = (state.ui.spinIndex + 1) % SPIN_FRAMES.length;
|
|
330
|
+
const frame = SPIN_FRAMES[state.ui.spinIndex];
|
|
331
|
+
const t = state.ui.spinText ? state.ui.spinText : "running";
|
|
332
|
+
$("queuePill").textContent = `[${frame}] ${t}...`;
|
|
333
|
+
}, 80);
|
|
334
|
+
}
|
|
335
|
+
function stopSpinner() {
|
|
336
|
+
if (state.ui.spinTimer != null) {
|
|
337
|
+
window.clearInterval(state.ui.spinTimer);
|
|
338
|
+
state.ui.spinTimer = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function getMarkdownCached(rel) {
|
|
342
|
+
const key = String(rel || "");
|
|
343
|
+
if (!key) return "";
|
|
344
|
+
if (state.mdCache.has(key)) return state.mdCache.get(key) || "";
|
|
345
|
+
try {
|
|
346
|
+
const t = await api(`/api/file/text?path=${encodeURIComponent(key)}&max=200000`);
|
|
347
|
+
const txt = typeof t?.text === "string" ? String(t.text) : "";
|
|
348
|
+
state.mdCache.set(key, txt);
|
|
349
|
+
return txt;
|
|
350
|
+
} catch {
|
|
351
|
+
state.mdCache.set(key, "");
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function guessExt(rel) {
|
|
356
|
+
const s = String(rel || "").toLowerCase();
|
|
357
|
+
return s;
|
|
358
|
+
}
|
|
359
|
+
function isImagePath(rel) {
|
|
360
|
+
const s = guessExt(rel);
|
|
361
|
+
return s.endsWith(".png") || s.endsWith(".jpg") || s.endsWith(".jpeg") || s.endsWith(".webp") || s.endsWith(".gif");
|
|
362
|
+
}
|
|
363
|
+
function isVideoPath(rel) {
|
|
364
|
+
const s = guessExt(rel);
|
|
365
|
+
return s.endsWith(".mp4") || s.endsWith(".webm") || s.endsWith(".mov");
|
|
366
|
+
}
|
|
367
|
+
function isAudioPath(rel) {
|
|
368
|
+
const s = guessExt(rel);
|
|
369
|
+
return s.endsWith(".wav") || s.endsWith(".mp3") || s.endsWith(".m4a") || s.endsWith(".ogg");
|
|
370
|
+
}
|
|
371
|
+
function isMarkdownPath(rel) {
|
|
372
|
+
const s = guessExt(rel);
|
|
373
|
+
return s.endsWith(".md") || s.endsWith(".markdown");
|
|
374
|
+
}
|
|
375
|
+
function renderRuns(list) {
|
|
376
|
+
$("runsPill").textContent = String(list.length);
|
|
377
|
+
const box = $("runsList");
|
|
378
|
+
box.innerHTML = "";
|
|
379
|
+
const tasks = [];
|
|
380
|
+
for (const r of list) {
|
|
381
|
+
const it = document.createElement("div");
|
|
382
|
+
it.className = "item";
|
|
383
|
+
const status = r.status === "running" ? "hot" : r.status === "failed" ? "danger" : "";
|
|
384
|
+
const runId = String(r.runId || "");
|
|
385
|
+
if (runId) it.id = "run-" + runId;
|
|
386
|
+
if (state.ui.selectedRunId && String(state.ui.selectedRunId) === runId) it.style.borderColor = "var(--hot)";
|
|
387
|
+
const filesAll = state.runFilesById[runId] || [];
|
|
388
|
+
const filesPrimary = filesAll.filter((f) => String(f?.kind || "").trim().toLowerCase() === "primary");
|
|
389
|
+
const isCompleted = String(r.status || "") === "completed";
|
|
390
|
+
const cmdId = String(r.commandId || "").trim().toLowerCase();
|
|
391
|
+
const defaultPrimaryOnly = isCompleted && (cmdId === "film" || cmdId === "slides" || cmdId === "proposal" || cmdId === "docs");
|
|
392
|
+
const showAll = Boolean(state.ui.showAllArtifactsByRunId[runId]);
|
|
393
|
+
const files = defaultPrimaryOnly && !showAll && filesPrimary.length > 0 ? filesPrimary : filesAll;
|
|
394
|
+
const hasText = Boolean(r && r.lastOutput && r.lastOutput.text);
|
|
395
|
+
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
396
|
+
const head = document.createElement("div");
|
|
397
|
+
head.className = "k";
|
|
398
|
+
head.innerHTML = `/${escapeHtml(r.commandId)} <span class="pill ${status}">${escapeHtml(r.status)}</span>`;
|
|
399
|
+
it.appendChild(head);
|
|
400
|
+
const meta = document.createElement("div");
|
|
401
|
+
meta.className = "v";
|
|
402
|
+
meta.textContent = `runId=${runId}
|
|
403
|
+
updatedAt=${String(r.updatedAt || "")}
|
|
404
|
+
input=${String(r.input || "")}`;
|
|
405
|
+
it.appendChild(meta);
|
|
406
|
+
const out = document.createElement("div");
|
|
407
|
+
out.className = "v";
|
|
408
|
+
out.style.marginTop = "8px";
|
|
409
|
+
if (String(r.status || "") === "running" && !hasText && !hasFiles) {
|
|
410
|
+
out.textContent = "running...";
|
|
411
|
+
} else {
|
|
412
|
+
out.textContent = hasText ? String(r.lastOutput.text) : "(no output)";
|
|
413
|
+
}
|
|
414
|
+
it.appendChild(out);
|
|
415
|
+
const cmdRow = document.createElement("div");
|
|
416
|
+
cmdRow.className = "cmd";
|
|
417
|
+
cmdRow.style.marginTop = "8px";
|
|
418
|
+
const openRunBtn = document.createElement("button");
|
|
419
|
+
openRunBtn.textContent = "Open folder";
|
|
420
|
+
openRunBtn.addEventListener("click", (e) => {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
e.stopPropagation();
|
|
423
|
+
if (!runId) return;
|
|
424
|
+
void api("/api/open", {
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers: { "content-type": "application/json" },
|
|
427
|
+
body: JSON.stringify({ path: `artifacts/maria-lite/runs/${runId}`, mode: "folder" })
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
cmdRow.appendChild(openRunBtn);
|
|
431
|
+
if (defaultPrimaryOnly && filesPrimary.length > 0 && filesAll.length > filesPrimary.length) {
|
|
432
|
+
const toggleBtn = document.createElement("button");
|
|
433
|
+
toggleBtn.textContent = showAll ? "Hide intermediates" : "Show intermediates";
|
|
434
|
+
toggleBtn.addEventListener("click", (e) => {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
e.stopPropagation();
|
|
437
|
+
if (!runId) return;
|
|
438
|
+
state.ui.showAllArtifactsByRunId[runId] = !Boolean(state.ui.showAllArtifactsByRunId[runId]);
|
|
439
|
+
void renderRuns(state.runs);
|
|
440
|
+
});
|
|
441
|
+
cmdRow.appendChild(toggleBtn);
|
|
442
|
+
}
|
|
443
|
+
it.appendChild(cmdRow);
|
|
444
|
+
it.addEventListener("click", () => void showRunDetail(runId));
|
|
445
|
+
if (files.length) {
|
|
446
|
+
const mrow = document.createElement("div");
|
|
447
|
+
mrow.className = "mediaRow";
|
|
448
|
+
mrow.style.marginTop = "10px";
|
|
449
|
+
for (const f of files) {
|
|
450
|
+
const rel = String(f.rel || "");
|
|
451
|
+
const card = document.createElement("div");
|
|
452
|
+
card.className = "thumb";
|
|
453
|
+
card.style.padding = "8px";
|
|
454
|
+
const top = document.createElement("div");
|
|
455
|
+
top.className = "k";
|
|
456
|
+
top.innerHTML = `file <span class="pill">${escapeHtml(f.kind || "file")}</span>`;
|
|
457
|
+
const link = document.createElement("div");
|
|
458
|
+
link.className = "v";
|
|
459
|
+
link.textContent = rel;
|
|
460
|
+
card.appendChild(top);
|
|
461
|
+
card.appendChild(link);
|
|
462
|
+
if (isImagePath(rel)) {
|
|
463
|
+
const img = document.createElement("img");
|
|
464
|
+
img.src = `/api/file?path=${encodeURIComponent(rel)}`;
|
|
465
|
+
img.loading = "lazy";
|
|
466
|
+
card.appendChild(img);
|
|
467
|
+
} else if (isVideoPath(rel)) {
|
|
468
|
+
const v = document.createElement("video");
|
|
469
|
+
v.src = `/api/file?path=${encodeURIComponent(rel)}`;
|
|
470
|
+
v.controls = true;
|
|
471
|
+
v.preload = "metadata";
|
|
472
|
+
card.appendChild(v);
|
|
473
|
+
} else if (isAudioPath(rel)) {
|
|
474
|
+
const a = document.createElement("audio");
|
|
475
|
+
a.src = `/api/file?path=${encodeURIComponent(rel)}`;
|
|
476
|
+
a.controls = true;
|
|
477
|
+
a.preload = "metadata";
|
|
478
|
+
card.appendChild(a);
|
|
479
|
+
} else if (isMarkdownPath(rel)) {
|
|
480
|
+
const mdBox = document.createElement("div");
|
|
481
|
+
mdBox.className = "md";
|
|
482
|
+
mdBox.textContent = "loading...";
|
|
483
|
+
card.appendChild(mdBox);
|
|
484
|
+
tasks.push(
|
|
485
|
+
(async () => {
|
|
486
|
+
const raw = await getMarkdownCached(rel);
|
|
487
|
+
mdBox.innerHTML = raw ? mdToHtml(raw) : "(empty markdown)";
|
|
488
|
+
})()
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const btnRow = document.createElement("div");
|
|
492
|
+
btnRow.className = "cmd";
|
|
493
|
+
btnRow.style.marginTop = "8px";
|
|
494
|
+
const openBtn = document.createElement("button");
|
|
495
|
+
openBtn.textContent = "Open folder";
|
|
496
|
+
openBtn.addEventListener("click", (e) => {
|
|
497
|
+
e.preventDefault();
|
|
498
|
+
e.stopPropagation();
|
|
499
|
+
void api("/api/open", {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: { "content-type": "application/json" },
|
|
502
|
+
body: JSON.stringify({ path: rel, mode: "folder" })
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
btnRow.appendChild(openBtn);
|
|
506
|
+
card.appendChild(btnRow);
|
|
507
|
+
mrow.appendChild(card);
|
|
508
|
+
}
|
|
509
|
+
it.appendChild(mrow);
|
|
510
|
+
}
|
|
511
|
+
box.appendChild(it);
|
|
512
|
+
}
|
|
513
|
+
return Promise.all(tasks).then(() => {
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
function renderLogs(lines) {
|
|
517
|
+
$("logsPill").textContent = String(lines.length);
|
|
518
|
+
$("logsBox").textContent = lines.map((l) => JSON.stringify(l)).join("\n");
|
|
519
|
+
}
|
|
520
|
+
function renderHistory(list) {
|
|
521
|
+
$("histPill").textContent = String(list.length);
|
|
522
|
+
const box = $("hist");
|
|
523
|
+
box.innerHTML = "";
|
|
524
|
+
for (const h of list) {
|
|
525
|
+
const it = document.createElement("div");
|
|
526
|
+
it.className = "item";
|
|
527
|
+
it.innerHTML = `<div class="k">cmd</div><div class="v">${escapeHtml(h)}</div>`;
|
|
528
|
+
it.addEventListener("click", () => {
|
|
529
|
+
("cmdInput" in window ? $("cmdInput") : null).value = h;
|
|
530
|
+
("cmdInput" in window ? $("cmdInput") : null).focus();
|
|
531
|
+
});
|
|
532
|
+
box.appendChild(it);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function renderJobs(list) {
|
|
536
|
+
$("jobsPill").textContent = String(list.length);
|
|
537
|
+
if (state.ui.quitting) {
|
|
538
|
+
stopSpinner();
|
|
539
|
+
$("queuePill").textContent = "bye";
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const q = list.filter((j) => j.status === "queued").length;
|
|
543
|
+
const r = list.filter((j) => j.status === "running").length;
|
|
544
|
+
if (r > 0) {
|
|
545
|
+
const j = list.slice().reverse().find((x) => x && x.status === "running") || null;
|
|
546
|
+
const t = j && typeof j.spinnerText === "string" && String(j.spinnerText).trim() ? String(j.spinnerText).trim() : "MARIA is working";
|
|
547
|
+
setSpinner(t);
|
|
548
|
+
} else {
|
|
549
|
+
stopSpinner();
|
|
550
|
+
$("queuePill").textContent = q ? `queued:${q}` : "idle";
|
|
551
|
+
}
|
|
552
|
+
const box = $("jobs");
|
|
553
|
+
box.innerHTML = "";
|
|
554
|
+
for (const j of list.slice().reverse().slice(0, 8)) {
|
|
555
|
+
const status = j.status === "running" ? "hot" : j.status === "error" ? "danger" : "";
|
|
556
|
+
const it = document.createElement("div");
|
|
557
|
+
it.className = "item";
|
|
558
|
+
it.innerHTML = `<div class="k">job ${escapeHtml(j.id)} <span class="pill ${status}">${escapeHtml(j.status)}</span></div><div class="v">${escapeHtml(j.line)}${j.runId ? `
|
|
559
|
+
runId=${escapeHtml(j.runId)}` : ""}</div>`;
|
|
560
|
+
box.appendChild(it);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function showRunDetail(runId) {
|
|
564
|
+
const prev = state.ui.selectedRunId ? String(state.ui.selectedRunId) : "";
|
|
565
|
+
const next = String(runId || "");
|
|
566
|
+
state.ui.selectedRunId = next;
|
|
567
|
+
state.ui.lastDetailRunId = next;
|
|
568
|
+
if (prev && prev !== next) {
|
|
569
|
+
const p = document.getElementById("run-" + prev);
|
|
570
|
+
if (p) p.style.borderColor = "";
|
|
571
|
+
}
|
|
572
|
+
if (next) {
|
|
573
|
+
const n = document.getElementById("run-" + next);
|
|
574
|
+
if (n) n.style.borderColor = "var(--hot)";
|
|
575
|
+
}
|
|
576
|
+
const el = next ? document.getElementById("run-" + next) : null;
|
|
577
|
+
if (el && typeof el.scrollIntoView === "function") {
|
|
578
|
+
try {
|
|
579
|
+
el.scrollIntoView({ block: "nearest" });
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function ensureDefaultRunDetail() {
|
|
585
|
+
const list = state.runs || [];
|
|
586
|
+
if (!Array.isArray(list) || list.length === 0) return;
|
|
587
|
+
const wanted = state.ui.selectedRunId ? String(state.ui.selectedRunId) : String(list[0].runId || "");
|
|
588
|
+
if (wanted) state.ui.selectedRunId = wanted;
|
|
589
|
+
}
|
|
590
|
+
function arrayBufferToBase64(buf) {
|
|
591
|
+
const bytes = new Uint8Array(buf);
|
|
592
|
+
let bin = "";
|
|
593
|
+
const chunk = 32768;
|
|
594
|
+
for (let i = 0; i < bytes.length; i += chunk) bin += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
595
|
+
return btoa(bin);
|
|
596
|
+
}
|
|
597
|
+
function setAvatarBadge(show) {
|
|
598
|
+
const b = document.getElementById("avatarBadge");
|
|
599
|
+
if (!b) return;
|
|
600
|
+
b.style.display = show ? "flex" : "none";
|
|
601
|
+
}
|
|
602
|
+
async function refreshAvatar() {
|
|
603
|
+
try {
|
|
604
|
+
const meta = await api(`/api/avatar/meta?ts=${Date.now()}`);
|
|
605
|
+
const has = Boolean(meta && meta.hasAvatar);
|
|
606
|
+
state.avatar.has = has;
|
|
607
|
+
setAvatarBadge(!has);
|
|
608
|
+
const img = $("avatarImg");
|
|
609
|
+
if (has) img.src = `/api/avatar?ts=${Date.now()}`;
|
|
610
|
+
else img.removeAttribute("src");
|
|
611
|
+
} catch {
|
|
612
|
+
state.avatar.has = false;
|
|
613
|
+
setAvatarBadge(true);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function runCmd(line) {
|
|
617
|
+
const s = String(line || "").trim();
|
|
618
|
+
if (!s) return;
|
|
619
|
+
const normalized = s.startsWith("/") ? s.slice(1).trim().toLowerCase() : s.toLowerCase();
|
|
620
|
+
if (normalized === "quit") state.ui.quitting = true;
|
|
621
|
+
const res = await api("/api/command", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ line: s }) });
|
|
622
|
+
$("cmdInput").value = "";
|
|
623
|
+
state.histIdx = -1;
|
|
624
|
+
if (normalized === "quit") {
|
|
625
|
+
if (state.ui.refreshTimer != null) window.clearInterval(state.ui.refreshTimer);
|
|
626
|
+
stopSpinner();
|
|
627
|
+
$("queuePill").textContent = "bye";
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
await refresh();
|
|
631
|
+
if (/^\/universe\s+multiverse\b/i.test(s)) {
|
|
632
|
+
const jobId = res && typeof res === "object" ? String(res.jobId || res.jobID || "") : "";
|
|
633
|
+
const waitForJob = async () => {
|
|
634
|
+
if (!jobId) return;
|
|
635
|
+
const deadline = Date.now() + 15e3;
|
|
636
|
+
while (Date.now() < deadline) {
|
|
637
|
+
try {
|
|
638
|
+
const jobs = await api("/api/jobs");
|
|
639
|
+
const items = jobs && Array.isArray(jobs.items) ? jobs.items : [];
|
|
640
|
+
const j = items.find((x) => String(x?.id || "") === jobId);
|
|
641
|
+
const st = j ? String(j.status || "") : "";
|
|
642
|
+
if (st === "done" || st === "failed" || st === "cancelled") return;
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
await new Promise((r) => window.setTimeout(r, 250));
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
await waitForJob();
|
|
649
|
+
await loadMultiverse().catch(() => {
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function applyUniverseTransform(ctx) {
|
|
654
|
+
const v = state.uni.view;
|
|
655
|
+
ctx.setTransform(v.scale, 0, 0, v.scale, v.tx, v.ty);
|
|
656
|
+
}
|
|
657
|
+
function cssVar(name, fallback) {
|
|
658
|
+
try {
|
|
659
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
660
|
+
return v || fallback;
|
|
661
|
+
} catch {
|
|
662
|
+
return fallback;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function parseRgbTriplet(s) {
|
|
666
|
+
const t = String(s || "").trim();
|
|
667
|
+
const m = /^#([0-9a-f]{6})$/i.exec(t);
|
|
668
|
+
if (m) {
|
|
669
|
+
const hex = m[1].toLowerCase();
|
|
670
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
671
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
672
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
673
|
+
return { r, g, b };
|
|
674
|
+
}
|
|
675
|
+
const m2 = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i.exec(t);
|
|
676
|
+
if (m2) return { r: Number(m2[1]), g: Number(m2[2]), b: Number(m2[3]) };
|
|
677
|
+
return { r: 0, g: 0, b: 0 };
|
|
678
|
+
}
|
|
679
|
+
function rgbaFromCssVar(name, alpha, fallbackHex = "#000000") {
|
|
680
|
+
const { r, g, b } = parseRgbTriplet(cssVar(name, fallbackHex));
|
|
681
|
+
const a = Math.max(0, Math.min(1, Number(alpha)));
|
|
682
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
683
|
+
}
|
|
684
|
+
function refreshCanvasSize(canvas) {
|
|
685
|
+
const rect = canvas.getBoundingClientRect();
|
|
686
|
+
const w = Math.max(2, Math.floor(rect.width));
|
|
687
|
+
const h = Math.max(2, Math.floor(rect.height));
|
|
688
|
+
const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
|
|
689
|
+
const bw = Math.floor(w * dpr);
|
|
690
|
+
const bh = Math.floor(h * dpr);
|
|
691
|
+
if (canvas.width !== bw) canvas.width = bw;
|
|
692
|
+
if (canvas.height !== bh) canvas.height = bh;
|
|
693
|
+
}
|
|
694
|
+
function screenToWorld(x, y) {
|
|
695
|
+
const v = state.uni.view;
|
|
696
|
+
const s = Number(v.scale || 1) || 1;
|
|
697
|
+
return { x: (x - Number(v.tx || 0)) / s, y: (y - Number(v.ty || 0)) / s };
|
|
698
|
+
}
|
|
699
|
+
function flattenNodes(root) {
|
|
700
|
+
const out = [];
|
|
701
|
+
const walk = (n, depth, parentId) => {
|
|
702
|
+
if (!n) return;
|
|
703
|
+
out.push({
|
|
704
|
+
id: String(n.id || ""),
|
|
705
|
+
name: String(n.name || ""),
|
|
706
|
+
level: String(n.level || ""),
|
|
707
|
+
topic: String(n.topic || ""),
|
|
708
|
+
depth,
|
|
709
|
+
parentId: parentId || null,
|
|
710
|
+
childIds: Array.isArray(n.children) ? n.children.map((c) => String(c.id || "")) : []
|
|
711
|
+
});
|
|
712
|
+
const kids = Array.isArray(n.children) ? n.children : [];
|
|
713
|
+
for (const k of kids) walk(k, depth + 1, String(n.id || ""));
|
|
714
|
+
};
|
|
715
|
+
walk(root, 0, null);
|
|
716
|
+
return out.filter((x) => x.id);
|
|
717
|
+
}
|
|
718
|
+
function flattenFlowNodes(spec) {
|
|
719
|
+
const bp = spec && (spec.blueprint || spec.flowBlueprint || spec.flow?.blueprint) ? spec.blueprint || spec.flowBlueprint || spec.flow?.blueprint : null;
|
|
720
|
+
const nodesRaw = bp && Array.isArray(bp.nodes) ? bp.nodes : [];
|
|
721
|
+
const edgesRaw = bp && Array.isArray(bp.edges) ? bp.edges : [];
|
|
722
|
+
const wf = spec && (spec.workflow || spec.flowWorkflow || spec.flow?.workflow) ? spec.workflow || spec.flowWorkflow || spec.flow?.workflow : null;
|
|
723
|
+
const wfSteps = wf && Array.isArray(wf.phases) ? wf.phases.flatMap((p) => Array.isArray(p?.steps) ? p.steps : []) : [];
|
|
724
|
+
const stepById = /* @__PURE__ */ new Map();
|
|
725
|
+
for (const s of wfSteps) {
|
|
726
|
+
const id = String(s?.id || "").trim();
|
|
727
|
+
if (id) stepById.set(id, s);
|
|
728
|
+
}
|
|
729
|
+
const inDeg = /* @__PURE__ */ new Map();
|
|
730
|
+
const outDeg = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const n of nodesRaw) {
|
|
732
|
+
const id = String(n?.id || "").trim();
|
|
733
|
+
if (!id) continue;
|
|
734
|
+
inDeg.set(id, 0);
|
|
735
|
+
outDeg.set(id, 0);
|
|
736
|
+
}
|
|
737
|
+
for (const e of edgesRaw) {
|
|
738
|
+
const from = String(e?.from || "").trim();
|
|
739
|
+
const to = String(e?.to || "").trim();
|
|
740
|
+
if (!from || !to) continue;
|
|
741
|
+
inDeg.set(to, (inDeg.get(to) ?? 0) + 1);
|
|
742
|
+
outDeg.set(from, (outDeg.get(from) ?? 0) + 1);
|
|
743
|
+
}
|
|
744
|
+
const startIds = Array.from(inDeg.entries()).filter(([, d]) => d === 0).map(([id]) => id).sort((a, b) => a.localeCompare(b));
|
|
745
|
+
const endIds = Array.from(outDeg.entries()).filter(([, d]) => d === 0).map(([id]) => id).sort((a, b) => a.localeCompare(b));
|
|
746
|
+
const nodes = nodesRaw.map((n) => {
|
|
747
|
+
const id = String(n?.id || "").trim();
|
|
748
|
+
const title = String(n?.title || "").trim();
|
|
749
|
+
const lane = String(n?.lane || "").trim();
|
|
750
|
+
const type = String(n?.type || "").trim();
|
|
751
|
+
const actionId = String(n?.actionId || "").trim();
|
|
752
|
+
const step = actionId ? stepById.get(actionId) : null;
|
|
753
|
+
const cmd = step && step.slash && typeof step.slash.cmd === "string" ? String(step.slash.cmd).trim() : "";
|
|
754
|
+
const hasWhen = Boolean(step && step.when);
|
|
755
|
+
const checks = step && step.input && typeof step.input === "object" ? step.input.checks : null;
|
|
756
|
+
const hasChecks = Array.isArray(checks) && checks.length > 0;
|
|
757
|
+
const fix = step && step.input && typeof step.input === "object" ? step.input.fix : null;
|
|
758
|
+
const maxFixAttempts = fix && (typeof fix.maxAttempts === "number" || typeof fix.maxAttempts === "string") ? Math.max(0, Math.min(5, Math.floor(Number(fix.maxAttempts)))) : 0;
|
|
759
|
+
const reconsiderStepIds = fix && typeof fix === "object" && fix && fix.reconsider && typeof fix.reconsider === "object" ? Array.isArray(fix.reconsider.stepIds) ? fix.reconsider.stepIds.filter((x) => typeof x === "string").map((x) => String(x).trim()).filter(Boolean).slice(0, 8) : [] : [];
|
|
760
|
+
return {
|
|
761
|
+
id,
|
|
762
|
+
name: title || id,
|
|
763
|
+
level: lane ? `${lane}${type ? `/${type}` : ""}` : type || "",
|
|
764
|
+
topic: actionId ? `actionId=${actionId}${cmd ? ` cmd=/${cmd}` : ""}` : "",
|
|
765
|
+
depth: 0,
|
|
766
|
+
parentId: null,
|
|
767
|
+
childIds: [],
|
|
768
|
+
flow: { lane, type, actionId, cmd, hasWhen, hasChecks, maxFixAttempts, reconsiderStepIds },
|
|
769
|
+
isStart: startIds.includes(id),
|
|
770
|
+
isEnd: endIds.includes(id)
|
|
771
|
+
};
|
|
772
|
+
}).filter((x) => x.id);
|
|
773
|
+
const edges = edgesRaw.map((e) => ({
|
|
774
|
+
from: String(e?.from || "").trim(),
|
|
775
|
+
to: String(e?.to || "").trim(),
|
|
776
|
+
label: typeof e?.label === "string" ? String(e.label).trim() : "",
|
|
777
|
+
when: typeof e?.when === "string" ? String(e.when).trim() : ""
|
|
778
|
+
})).filter((e) => e.from && e.to);
|
|
779
|
+
return { nodes, edges, startIds, endIds };
|
|
780
|
+
}
|
|
781
|
+
function reorderFlowBucketsForBranching(params) {
|
|
782
|
+
const maxL = Math.max(0, ...Array.from(params.buckets.keys()));
|
|
783
|
+
const preds = /* @__PURE__ */ new Map();
|
|
784
|
+
const succs = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const [l, arr] of params.buckets) {
|
|
786
|
+
for (const id of arr) {
|
|
787
|
+
if (!preds.has(id)) preds.set(id, []);
|
|
788
|
+
if (!succs.has(id)) succs.set(id, []);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (const e of params.edges) {
|
|
792
|
+
if (!preds.has(e.to)) preds.set(e.to, []);
|
|
793
|
+
if (!succs.has(e.from)) succs.set(e.from, []);
|
|
794
|
+
preds.get(e.to).push(e.from);
|
|
795
|
+
succs.get(e.from).push(e.to);
|
|
796
|
+
}
|
|
797
|
+
for (const [k, v] of preds) preds.set(k, Array.from(new Set(v)).sort((a, b) => a.localeCompare(b)));
|
|
798
|
+
for (const [k, v] of succs) succs.set(k, Array.from(new Set(v)).sort((a, b) => a.localeCompare(b)));
|
|
799
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const [l, arr] of params.buckets) buckets.set(l, [...arr]);
|
|
801
|
+
const posIndex = /* @__PURE__ */ new Map();
|
|
802
|
+
const refreshPosIndex = () => {
|
|
803
|
+
posIndex.clear();
|
|
804
|
+
for (const [l, arr] of buckets) {
|
|
805
|
+
for (let i = 0; i < arr.length; i++) posIndex.set(arr[i], i);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
refreshPosIndex();
|
|
809
|
+
const bary = (ids, neighborMap) => {
|
|
810
|
+
const scored = ids.map((id) => {
|
|
811
|
+
const ns = neighborMap.get(id) || [];
|
|
812
|
+
const vals = ns.map((n) => posIndex.get(n)).filter((x) => typeof x === "number");
|
|
813
|
+
const score = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : Number.POSITIVE_INFINITY;
|
|
814
|
+
return { id, score };
|
|
815
|
+
});
|
|
816
|
+
scored.sort((a, b) => a.score - b.score || a.id.localeCompare(b.id));
|
|
817
|
+
return scored.map((s) => s.id);
|
|
818
|
+
};
|
|
819
|
+
for (let l = 1; l <= maxL; l++) {
|
|
820
|
+
const arr = buckets.get(l) || [];
|
|
821
|
+
buckets.set(l, bary(arr, preds));
|
|
822
|
+
refreshPosIndex();
|
|
823
|
+
}
|
|
824
|
+
for (let l = maxL - 1; l >= 0; l--) {
|
|
825
|
+
const arr = buckets.get(l) || [];
|
|
826
|
+
buckets.set(l, bary(arr, succs));
|
|
827
|
+
refreshPosIndex();
|
|
828
|
+
}
|
|
829
|
+
return buckets;
|
|
830
|
+
}
|
|
831
|
+
function computeFlowTopo(nodes, edges) {
|
|
832
|
+
const ids = nodes.map((n) => String(n.id)).filter(Boolean);
|
|
833
|
+
const inDeg = new Map(ids.map((id) => [id, 0]));
|
|
834
|
+
const outs = new Map(ids.map((id) => [id, []]));
|
|
835
|
+
for (const e of edges) {
|
|
836
|
+
if (!inDeg.has(e.to)) inDeg.set(e.to, 0);
|
|
837
|
+
if (!outs.has(e.from)) outs.set(e.from, []);
|
|
838
|
+
inDeg.set(e.to, (inDeg.get(e.to) ?? 0) + 1);
|
|
839
|
+
outs.get(e.from).push(e.to);
|
|
840
|
+
}
|
|
841
|
+
for (const [k, v] of outs) {
|
|
842
|
+
v.sort((a, b) => a.localeCompare(b));
|
|
843
|
+
outs.set(k, v);
|
|
844
|
+
}
|
|
845
|
+
const levelById = /* @__PURE__ */ new Map();
|
|
846
|
+
const q = Array.from(inDeg.entries()).filter(([, d]) => d === 0).map(([id]) => id).sort((a, b) => a.localeCompare(b));
|
|
847
|
+
for (const id of q) levelById.set(id, 0);
|
|
848
|
+
const inDeg2 = new Map(inDeg);
|
|
849
|
+
const queue = [...q];
|
|
850
|
+
while (queue.length) {
|
|
851
|
+
const cur = queue.shift();
|
|
852
|
+
const curL = levelById.get(cur) ?? 0;
|
|
853
|
+
for (const nxt of outs.get(cur) || []) {
|
|
854
|
+
levelById.set(nxt, Math.max(levelById.get(nxt) ?? 0, curL + 1));
|
|
855
|
+
inDeg2.set(nxt, (inDeg2.get(nxt) ?? 0) - 1);
|
|
856
|
+
if ((inDeg2.get(nxt) ?? 0) === 0) {
|
|
857
|
+
queue.push(nxt);
|
|
858
|
+
queue.sort((a, b) => a.localeCompare(b));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const maxLevel = Math.max(0, ...ids.map((id) => levelById.get(id) ?? 0));
|
|
863
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
864
|
+
for (const id of ids) {
|
|
865
|
+
const l = levelById.get(id) ?? 0;
|
|
866
|
+
const arr = buckets.get(l) || [];
|
|
867
|
+
arr.push(id);
|
|
868
|
+
buckets.set(l, arr);
|
|
869
|
+
}
|
|
870
|
+
for (const [l, arr] of buckets) {
|
|
871
|
+
arr.sort((a, b) => a.localeCompare(b));
|
|
872
|
+
buckets.set(l, arr);
|
|
873
|
+
}
|
|
874
|
+
const reordered = reorderFlowBucketsForBranching({ buckets, edges });
|
|
875
|
+
for (const [l, arr] of reordered) buckets.set(l, arr);
|
|
876
|
+
const bucketSizeById = /* @__PURE__ */ new Map();
|
|
877
|
+
for (const [, arr] of buckets) {
|
|
878
|
+
for (const id of arr) bucketSizeById.set(id, arr.length);
|
|
879
|
+
}
|
|
880
|
+
return { levelById, buckets, bucketSizeById, maxLevel };
|
|
881
|
+
}
|
|
882
|
+
function layoutFlowDAG(nodes, edges, opts) {
|
|
883
|
+
const topo = computeFlowTopo(nodes, edges);
|
|
884
|
+
const maxL = topo.maxLevel;
|
|
885
|
+
const canvas = $("uniCanvas");
|
|
886
|
+
const w = canvas.width;
|
|
887
|
+
const h = canvas.height;
|
|
888
|
+
const padX = Math.max(60, Math.floor(w * 0.06));
|
|
889
|
+
const padY = Math.max(60, Math.floor(h * 0.08));
|
|
890
|
+
const usableW = Math.max(1, w - padX * 2);
|
|
891
|
+
const usableH = Math.max(1, h - padY * 2);
|
|
892
|
+
const dir = opts?.dir === "vertical" ? "vertical" : "horizontal";
|
|
893
|
+
const dy = maxL > 0 ? usableH / maxL : 0;
|
|
894
|
+
const dx = maxL > 0 ? usableW / maxL : 0;
|
|
895
|
+
const pos = /* @__PURE__ */ new Map();
|
|
896
|
+
for (let l = 0; l <= maxL; l++) {
|
|
897
|
+
const arr = topo.buckets.get(l) || [];
|
|
898
|
+
if (!arr.length) continue;
|
|
899
|
+
if (dir === "vertical") {
|
|
900
|
+
const y = padY + dy * l;
|
|
901
|
+
const sx = arr.length > 1 ? usableW / (arr.length - 1) : 0;
|
|
902
|
+
for (let i = 0; i < arr.length; i++) {
|
|
903
|
+
const x = padX + (arr.length > 1 ? sx * i : usableW / 2);
|
|
904
|
+
pos.set(arr[i], { x, y });
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
const x = padX + dx * l;
|
|
908
|
+
const sy = arr.length > 1 ? usableH / (arr.length - 1) : 0;
|
|
909
|
+
for (let i = 0; i < arr.length; i++) {
|
|
910
|
+
const y = padY + (arr.length > 1 ? sy * i : usableH / 2);
|
|
911
|
+
pos.set(arr[i], { x, y });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return { pos, topo };
|
|
916
|
+
}
|
|
917
|
+
function layoutTree(nodes, centerId) {
|
|
918
|
+
const levels = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const n of nodes) {
|
|
920
|
+
const d = Number.isFinite(n.depth) ? n.depth : 0;
|
|
921
|
+
const arr = levels.get(d) || [];
|
|
922
|
+
arr.push(n);
|
|
923
|
+
levels.set(d, arr);
|
|
924
|
+
}
|
|
925
|
+
for (const [d, arr] of levels) {
|
|
926
|
+
arr.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
927
|
+
levels.set(d, arr);
|
|
928
|
+
}
|
|
929
|
+
const maxD = Math.max(0, ...Array.from(levels.keys()));
|
|
930
|
+
const canvas = $("uniCanvas");
|
|
931
|
+
const w = canvas.width;
|
|
932
|
+
const h = canvas.height;
|
|
933
|
+
const cx = w / 2;
|
|
934
|
+
const cy = h / 2;
|
|
935
|
+
const r0 = Math.min(w, h) * 0.12;
|
|
936
|
+
const dr = Math.min(w, h) * 0.12;
|
|
937
|
+
const pos = /* @__PURE__ */ new Map();
|
|
938
|
+
for (let d = 0; d <= maxD; d++) {
|
|
939
|
+
const ring = levels.get(d) || [];
|
|
940
|
+
if (!ring.length) continue;
|
|
941
|
+
const r = d === 0 ? 0 : r0 + dr * (d - 1);
|
|
942
|
+
for (let i = 0; i < ring.length; i++) {
|
|
943
|
+
const a = ring.length === 1 ? -Math.PI / 2 : Math.PI * 2 * i / ring.length - Math.PI / 2;
|
|
944
|
+
pos.set(ring[i].id, { x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (centerId && pos.has(centerId)) pos.set(centerId, { x: cx, y: cy });
|
|
948
|
+
return pos;
|
|
949
|
+
}
|
|
950
|
+
function drawReplayEdges(ctx, pos, edges, count) {
|
|
951
|
+
const lim = Math.max(0, Math.min(edges.length, count));
|
|
952
|
+
ctx.strokeStyle = rgbaFromCssVar("--hot", 0.85, "#00ff66");
|
|
953
|
+
ctx.lineWidth = 2;
|
|
954
|
+
for (let i = 0; i < lim; i++) {
|
|
955
|
+
const e = edges[i];
|
|
956
|
+
const p1 = pos.get(e.from);
|
|
957
|
+
const p2 = pos.get(e.to);
|
|
958
|
+
if (!p1 || !p2) continue;
|
|
959
|
+
ctx.beginPath();
|
|
960
|
+
ctx.moveTo(p1.x, p1.y);
|
|
961
|
+
ctx.lineTo(p2.x, p2.y);
|
|
962
|
+
ctx.stroke();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function pickLabel(base, maxLen) {
|
|
966
|
+
const s = String(base || "");
|
|
967
|
+
if (s.length <= maxLen) return s;
|
|
968
|
+
return s.slice(0, Math.max(0, maxLen - 1)).trimEnd() + "\u2026";
|
|
969
|
+
}
|
|
970
|
+
function placeLabel(ctx, x, y, r, text, used) {
|
|
971
|
+
const pad = 3;
|
|
972
|
+
const w = Math.ceil(ctx.measureText(text).width);
|
|
973
|
+
const h = 14;
|
|
974
|
+
const candidates = [
|
|
975
|
+
{ dx: r + 8, dy: 4 },
|
|
976
|
+
{ dx: -(r + 8 + w), dy: 4 },
|
|
977
|
+
{ dx: -(w / 2), dy: -(r + 10) },
|
|
978
|
+
{ dx: -(w / 2), dy: r + 16 }
|
|
979
|
+
];
|
|
980
|
+
for (const c of candidates) {
|
|
981
|
+
const x1 = x + c.dx - pad;
|
|
982
|
+
const y1 = y + c.dy - h + 2 - pad;
|
|
983
|
+
const x2 = x + c.dx + w + pad;
|
|
984
|
+
const y2 = y + c.dy + 2 + pad;
|
|
985
|
+
const hit = used.some((b) => !(x2 < b.x1 || x1 > b.x2 || y2 < b.y1 || y1 > b.y2));
|
|
986
|
+
if (!hit) {
|
|
987
|
+
used.push({ x1, y1, x2, y2 });
|
|
988
|
+
return { tx: x + c.dx, ty: y + c.dy };
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
used.push({ x1: x + r + 8 - pad, y1: y + 4 - h + 2 - pad, x2: x + r + 8 + w + pad, y2: y + 4 + 2 + pad });
|
|
992
|
+
return { tx: x + r + 8, ty: y + 4 };
|
|
993
|
+
}
|
|
994
|
+
function placeLabelFlow(ctx, x, y, r, text, used) {
|
|
995
|
+
const pad = 3;
|
|
996
|
+
const w = Math.ceil(ctx.measureText(text).width);
|
|
997
|
+
const h = 14;
|
|
998
|
+
const candidates = [
|
|
999
|
+
{ dx: -(w / 2), dy: -22 },
|
|
1000
|
+
// above
|
|
1001
|
+
{ dx: -(w / 2), dy: r + 18 }
|
|
1002
|
+
// below
|
|
1003
|
+
];
|
|
1004
|
+
for (const c of candidates) {
|
|
1005
|
+
const x1 = x + c.dx - pad;
|
|
1006
|
+
const y1 = y + c.dy - h + 2 - pad;
|
|
1007
|
+
const x2 = x + c.dx + w + pad;
|
|
1008
|
+
const y2 = y + c.dy + 2 + pad;
|
|
1009
|
+
const hit = used.some((b) => !(x2 < b.x1 || x1 > b.x2 || y2 < b.y1 || y1 > b.y2));
|
|
1010
|
+
if (!hit) {
|
|
1011
|
+
used.push({ x1, y1, x2, y2 });
|
|
1012
|
+
return { tx: x + c.dx, ty: y + c.dy };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
used.push({ x1: x - w / 2 - pad, y1: y - (r + 12) - h + 2 - pad, x2: x + w / 2 + pad, y2: y - (r + 12) + 2 + pad });
|
|
1016
|
+
return { tx: x - w / 2, ty: y - (r + 12) };
|
|
1017
|
+
}
|
|
1018
|
+
function drawLabelWithSelection(ctx, params) {
|
|
1019
|
+
const t = String(params.text || "");
|
|
1020
|
+
if (!t) return;
|
|
1021
|
+
if (!params.selected) {
|
|
1022
|
+
ctx.fillText(t, params.tx, params.ty);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const padX = 6;
|
|
1026
|
+
const padY = 4;
|
|
1027
|
+
const w = Math.ceil(ctx.measureText(t).width);
|
|
1028
|
+
const h = 14;
|
|
1029
|
+
const x = params.tx - padX;
|
|
1030
|
+
const y = params.ty - h + 2 - padY;
|
|
1031
|
+
const rr = 8;
|
|
1032
|
+
ctx.save();
|
|
1033
|
+
ctx.fillStyle = rgbaFromCssVar("--hot", 0.22, "#00ff66");
|
|
1034
|
+
ctx.strokeStyle = rgbaFromCssVar("--hot", 0.45, "#00ff66");
|
|
1035
|
+
ctx.lineWidth = 1;
|
|
1036
|
+
ctx.beginPath();
|
|
1037
|
+
ctx.moveTo(x + rr, y);
|
|
1038
|
+
ctx.lineTo(x + w + padX * 2 - rr, y);
|
|
1039
|
+
ctx.quadraticCurveTo(x + w + padX * 2, y, x + w + padX * 2, y + rr);
|
|
1040
|
+
ctx.lineTo(x + w + padX * 2, y + h + padY * 2 - rr);
|
|
1041
|
+
ctx.quadraticCurveTo(x + w + padX * 2, y + h + padY * 2, x + w + padX * 2 - rr, y + h + padY * 2);
|
|
1042
|
+
ctx.lineTo(x + rr, y + h + padY * 2);
|
|
1043
|
+
ctx.quadraticCurveTo(x, y + h + padY * 2, x, y + h + padY * 2 - rr);
|
|
1044
|
+
ctx.lineTo(x, y + rr);
|
|
1045
|
+
ctx.quadraticCurveTo(x, y, x + rr, y);
|
|
1046
|
+
ctx.closePath();
|
|
1047
|
+
ctx.fill();
|
|
1048
|
+
ctx.stroke();
|
|
1049
|
+
ctx.restore();
|
|
1050
|
+
ctx.save();
|
|
1051
|
+
ctx.fillStyle = cssVar("--fg", "#e4fce4");
|
|
1052
|
+
ctx.fillText(t, params.tx, params.ty);
|
|
1053
|
+
ctx.restore();
|
|
1054
|
+
}
|
|
1055
|
+
function drawUniverse(spec, contexts) {
|
|
1056
|
+
const canvas = $("uniCanvas");
|
|
1057
|
+
refreshCanvasSize(canvas);
|
|
1058
|
+
const ctx = canvas.getContext("2d");
|
|
1059
|
+
if (!ctx) return;
|
|
1060
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1061
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1062
|
+
ctx.save();
|
|
1063
|
+
applyUniverseTransform(ctx);
|
|
1064
|
+
if (spec && (spec.blueprint || spec.flowBlueprint || spec.flow?.blueprint)) {
|
|
1065
|
+
const f = flattenFlowNodes(spec);
|
|
1066
|
+
const flowDir = readFlowLayoutDir();
|
|
1067
|
+
const showParallel = readFlowParallelEnabled();
|
|
1068
|
+
const laid = layoutFlowDAG(f.nodes, f.edges, { dir: flowDir });
|
|
1069
|
+
const pos2 = laid.pos;
|
|
1070
|
+
state.uni.pos = pos2;
|
|
1071
|
+
state.uni.nodes = f.nodes;
|
|
1072
|
+
state.uni.edges = f.edges;
|
|
1073
|
+
state.uni.flowMeta = laid.topo;
|
|
1074
|
+
state.uni.seek = f.edges.length;
|
|
1075
|
+
$("uniSeek").max = String(Math.max(0, f.edges.length));
|
|
1076
|
+
$("uniSeek").value = String(f.edges.length);
|
|
1077
|
+
const idSet = new Set(f.nodes.map((n) => String(n.id)));
|
|
1078
|
+
const curSel = state.uni.selectedNodeId ? String(state.uni.selectedNodeId) : "";
|
|
1079
|
+
if (!curSel || !idSet.has(curSel)) {
|
|
1080
|
+
state.uni.selectedNodeId = f.startIds[0] || (f.nodes[0] ? String(f.nodes[0].id) : "");
|
|
1081
|
+
}
|
|
1082
|
+
const attemptByStepId = /* @__PURE__ */ new Map();
|
|
1083
|
+
const evs2 = contexts && Array.isArray(contexts.task) ? contexts.task : [];
|
|
1084
|
+
for (const ev of evs2) {
|
|
1085
|
+
if (!ev || typeof ev !== "object") continue;
|
|
1086
|
+
if (String(ev.kind || "") !== "agent.output") continue;
|
|
1087
|
+
const pld = ev.payload;
|
|
1088
|
+
const stepId = pld && typeof pld === "object" ? String(pld.stepId || "").trim() : "";
|
|
1089
|
+
const attempt = pld && typeof pld === "object" ? Number(pld.attempt) : NaN;
|
|
1090
|
+
if (!stepId) continue;
|
|
1091
|
+
if (!Number.isFinite(attempt)) continue;
|
|
1092
|
+
attemptByStepId.set(stepId, Math.max(attemptByStepId.get(stepId) ?? 0, Math.floor(attempt)));
|
|
1093
|
+
}
|
|
1094
|
+
const usedLabels2 = [];
|
|
1095
|
+
if (showParallel) {
|
|
1096
|
+
for (const [, arr] of laid.topo.buckets) {
|
|
1097
|
+
if (arr.length <= 1) continue;
|
|
1098
|
+
const p0 = pos2.get(arr[0]);
|
|
1099
|
+
if (!p0) continue;
|
|
1100
|
+
ctx.fillStyle = rgbaFromCssVar("--hot", 0.06, "#00ff66");
|
|
1101
|
+
if (flowDir === "vertical") {
|
|
1102
|
+
ctx.fillRect(-2e4, p0.y - 26, 4e4, 52);
|
|
1103
|
+
} else {
|
|
1104
|
+
ctx.fillRect(p0.x - 34, -2e4, 68, 4e4);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const drawArrow = (from, to, opts) => {
|
|
1109
|
+
const a = Math.max(0.05, Math.min(1, Number(opts?.alpha ?? 0.25)));
|
|
1110
|
+
ctx.strokeStyle = rgbaFromCssVar("--hot", a, "#00ff66");
|
|
1111
|
+
ctx.lineWidth = 1;
|
|
1112
|
+
ctx.setLineDash(opts?.dashed ? [6, 5] : []);
|
|
1113
|
+
ctx.beginPath();
|
|
1114
|
+
ctx.moveTo(from.x, from.y);
|
|
1115
|
+
ctx.lineTo(to.x, to.y);
|
|
1116
|
+
ctx.stroke();
|
|
1117
|
+
ctx.setLineDash([]);
|
|
1118
|
+
const dx = to.x - from.x;
|
|
1119
|
+
const dy = to.y - from.y;
|
|
1120
|
+
const ang = Math.atan2(dy, dx);
|
|
1121
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1122
|
+
if (len > 8) {
|
|
1123
|
+
const ah = 8;
|
|
1124
|
+
const tx = to.x - Math.cos(ang) * 10;
|
|
1125
|
+
const ty = to.y - Math.sin(ang) * 10;
|
|
1126
|
+
ctx.fillStyle = rgbaFromCssVar("--hot", a, "#00ff66");
|
|
1127
|
+
ctx.beginPath();
|
|
1128
|
+
ctx.moveTo(tx, ty);
|
|
1129
|
+
ctx.lineTo(tx - Math.cos(ang - Math.PI / 6) * ah, ty - Math.sin(ang - Math.PI / 6) * ah);
|
|
1130
|
+
ctx.lineTo(tx - Math.cos(ang + Math.PI / 6) * ah, ty - Math.sin(ang + Math.PI / 6) * ah);
|
|
1131
|
+
ctx.closePath();
|
|
1132
|
+
ctx.fill();
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
const drawSelfLoop = (p, r, label, alpha) => {
|
|
1136
|
+
const a = Math.max(0.06, Math.min(1, Number(alpha)));
|
|
1137
|
+
ctx.strokeStyle = rgbaFromCssVar("--muted", a, "#6fd76f");
|
|
1138
|
+
ctx.lineWidth = 1;
|
|
1139
|
+
ctx.setLineDash([6, 5]);
|
|
1140
|
+
const x1 = p.x + r;
|
|
1141
|
+
const y1 = p.y - r;
|
|
1142
|
+
const x2 = p.x + r;
|
|
1143
|
+
const y2 = p.y + r;
|
|
1144
|
+
const cx = p.x + r + 26;
|
|
1145
|
+
const cy = p.y;
|
|
1146
|
+
ctx.beginPath();
|
|
1147
|
+
ctx.moveTo(x1, y1);
|
|
1148
|
+
ctx.quadraticCurveTo(cx, cy, x2, y2);
|
|
1149
|
+
ctx.stroke();
|
|
1150
|
+
ctx.setLineDash([]);
|
|
1151
|
+
ctx.fillStyle = rgbaFromCssVar("--muted", a, "#6fd76f");
|
|
1152
|
+
ctx.beginPath();
|
|
1153
|
+
ctx.moveTo(x2, y2);
|
|
1154
|
+
ctx.lineTo(x2 - 8, y2 - 3);
|
|
1155
|
+
ctx.lineTo(x2 - 8, y2 + 3);
|
|
1156
|
+
ctx.closePath();
|
|
1157
|
+
ctx.fill();
|
|
1158
|
+
if (label) {
|
|
1159
|
+
ctx.save();
|
|
1160
|
+
ctx.font = "11px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1161
|
+
ctx.fillStyle = rgbaFromCssVar("--muted", a, "#6fd76f");
|
|
1162
|
+
ctx.fillText(pickLabel(label, 18), cx - 10, cy - 6);
|
|
1163
|
+
ctx.restore();
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
for (const e of f.edges) {
|
|
1167
|
+
const p1 = pos2.get(e.from);
|
|
1168
|
+
const p2 = pos2.get(e.to);
|
|
1169
|
+
if (!p1 || !p2) continue;
|
|
1170
|
+
const hasCond = Boolean(String(e.when || "").trim());
|
|
1171
|
+
drawArrow(p1, p2, { dashed: hasCond, alpha: 0.25 });
|
|
1172
|
+
const elabel = String(e.label || "").trim() || String(e.when || "").trim();
|
|
1173
|
+
if (elabel) {
|
|
1174
|
+
const mx = (p1.x + p2.x) / 2;
|
|
1175
|
+
const my = (p1.y + p2.y) / 2;
|
|
1176
|
+
ctx.save();
|
|
1177
|
+
ctx.font = "11px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1178
|
+
ctx.fillStyle = rgbaFromCssVar("--muted", 0.9, "#6fd76f");
|
|
1179
|
+
ctx.fillText(pickLabel(elabel, 22), mx + 6, my - 6);
|
|
1180
|
+
ctx.restore();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const loopInfo = [];
|
|
1184
|
+
const reconsiderEdges = [];
|
|
1185
|
+
const nodeIdByStepId = /* @__PURE__ */ new Map();
|
|
1186
|
+
for (const n of f.nodes) {
|
|
1187
|
+
const flowMeta = n.flow || {};
|
|
1188
|
+
const stepId = flowMeta && typeof flowMeta.actionId === "string" ? String(flowMeta.actionId).trim() : "";
|
|
1189
|
+
if (stepId) nodeIdByStepId.set(stepId, String(n.id));
|
|
1190
|
+
}
|
|
1191
|
+
const hit2 = [];
|
|
1192
|
+
ctx.font = "12px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1193
|
+
for (const n of f.nodes) {
|
|
1194
|
+
const p = pos2.get(n.id);
|
|
1195
|
+
if (!p) continue;
|
|
1196
|
+
const isSel = state.uni.selectedNodeId && String(state.uni.selectedNodeId) === String(n.id);
|
|
1197
|
+
const r = 10;
|
|
1198
|
+
const isStart = Boolean(n.isStart);
|
|
1199
|
+
const isEnd = Boolean(n.isEnd);
|
|
1200
|
+
const parN = laid.topo.bucketSizeById.get(String(n.id)) ?? 1;
|
|
1201
|
+
const flowMeta = n.flow || {};
|
|
1202
|
+
const hasWhen = Boolean(flowMeta.hasWhen);
|
|
1203
|
+
const hasChecks = Boolean(flowMeta.hasChecks);
|
|
1204
|
+
const maxFixAttempts = Number(flowMeta.maxFixAttempts || 0) || 0;
|
|
1205
|
+
const reconsiderStepIds = Array.isArray(flowMeta.reconsiderStepIds) ? flowMeta.reconsiderStepIds : [];
|
|
1206
|
+
const stepId = flowMeta && typeof flowMeta.actionId === "string" ? String(flowMeta.actionId).trim() : "";
|
|
1207
|
+
const ranAttempts = stepId ? attemptByStepId.get(stepId) ?? 0 : 0;
|
|
1208
|
+
if (maxFixAttempts > 0 || ranAttempts > 0) loopInfo.push({ nodeId: String(n.id), planned: maxFixAttempts, ran: ranAttempts });
|
|
1209
|
+
for (const rid of reconsiderStepIds) {
|
|
1210
|
+
if (typeof rid !== "string" || !rid.trim()) continue;
|
|
1211
|
+
if (stepId && String(rid).trim() === stepId) continue;
|
|
1212
|
+
const toNodeId = nodeIdByStepId.get(String(rid).trim());
|
|
1213
|
+
if (!toNodeId) continue;
|
|
1214
|
+
reconsiderEdges.push({ fromNodeId: String(n.id), toNodeId, label: "reconsider" });
|
|
1215
|
+
}
|
|
1216
|
+
ctx.fillStyle = cssVar("--canvasBg", "#030503");
|
|
1217
|
+
ctx.strokeStyle = isStart ? cssVar("--hot", "#00ff66") : isEnd ? cssVar("--muted", "#6fd76f") : cssVar("--hot", "#00ff66");
|
|
1218
|
+
ctx.lineWidth = isSel ? 4 : 2;
|
|
1219
|
+
if (isSel) {
|
|
1220
|
+
ctx.shadowColor = rgbaFromCssVar("--hot", 0.7, "#00ff66");
|
|
1221
|
+
ctx.shadowBlur = 14;
|
|
1222
|
+
} else {
|
|
1223
|
+
ctx.shadowColor = "transparent";
|
|
1224
|
+
ctx.shadowBlur = 0;
|
|
1225
|
+
}
|
|
1226
|
+
ctx.beginPath();
|
|
1227
|
+
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
1228
|
+
ctx.fill();
|
|
1229
|
+
ctx.stroke();
|
|
1230
|
+
ctx.shadowColor = "transparent";
|
|
1231
|
+
ctx.shadowBlur = 0;
|
|
1232
|
+
if (isStart) {
|
|
1233
|
+
ctx.fillStyle = rgbaFromCssVar("--hot", 0.9, "#00ff66");
|
|
1234
|
+
ctx.beginPath();
|
|
1235
|
+
ctx.moveTo(p.x, p.y - (r + 8));
|
|
1236
|
+
ctx.lineTo(p.x - 6, p.y - (r + 18));
|
|
1237
|
+
ctx.lineTo(p.x + 6, p.y - (r + 18));
|
|
1238
|
+
ctx.closePath();
|
|
1239
|
+
ctx.fill();
|
|
1240
|
+
}
|
|
1241
|
+
if (isEnd) {
|
|
1242
|
+
ctx.fillStyle = rgbaFromCssVar("--muted", 0.9, "#6fd76f");
|
|
1243
|
+
ctx.beginPath();
|
|
1244
|
+
ctx.moveTo(p.x, p.y + (r + 8));
|
|
1245
|
+
ctx.lineTo(p.x - 6, p.y + (r + 18));
|
|
1246
|
+
ctx.lineTo(p.x + 6, p.y + (r + 18));
|
|
1247
|
+
ctx.closePath();
|
|
1248
|
+
ctx.fill();
|
|
1249
|
+
}
|
|
1250
|
+
if (showParallel && parN > 1) {
|
|
1251
|
+
ctx.save();
|
|
1252
|
+
ctx.strokeStyle = rgbaFromCssVar("--muted", 0.9, "#6fd76f");
|
|
1253
|
+
ctx.lineWidth = 2;
|
|
1254
|
+
ctx.beginPath();
|
|
1255
|
+
ctx.moveTo(p.x - 3, p.y - 4);
|
|
1256
|
+
ctx.lineTo(p.x - 3, p.y + 4);
|
|
1257
|
+
ctx.moveTo(p.x + 3, p.y - 4);
|
|
1258
|
+
ctx.lineTo(p.x + 3, p.y + 4);
|
|
1259
|
+
ctx.stroke();
|
|
1260
|
+
ctx.restore();
|
|
1261
|
+
}
|
|
1262
|
+
{
|
|
1263
|
+
const marks = [];
|
|
1264
|
+
if (hasWhen) marks.push("?");
|
|
1265
|
+
if (hasChecks) marks.push("\u2713");
|
|
1266
|
+
if (maxFixAttempts > 0) marks.push(`\u21BB${maxFixAttempts}`);
|
|
1267
|
+
if (marks.length) {
|
|
1268
|
+
ctx.save();
|
|
1269
|
+
ctx.font = "11px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1270
|
+
ctx.fillStyle = rgbaFromCssVar("--muted", 0.95, "#6fd76f");
|
|
1271
|
+
ctx.fillText(marks.join(" "), p.x + r + 6, p.y + 4);
|
|
1272
|
+
ctx.restore();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (ranAttempts > 0) {
|
|
1276
|
+
ctx.save();
|
|
1277
|
+
ctx.font = "11px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1278
|
+
ctx.fillStyle = rgbaFromCssVar("--hot", 0.95, "#00ff66");
|
|
1279
|
+
ctx.fillText(`retry=${ranAttempts}`, p.x - r, p.y - (r + 10));
|
|
1280
|
+
ctx.restore();
|
|
1281
|
+
}
|
|
1282
|
+
const label = pickLabel(String(n.name || n.id), 26);
|
|
1283
|
+
ctx.fillStyle = cssVar("--text", "#e4fce4");
|
|
1284
|
+
const lp = placeLabelFlow(ctx, p.x, p.y, r, label, usedLabels2);
|
|
1285
|
+
drawLabelWithSelection(ctx, { tx: lp.tx, ty: lp.ty, text: label, selected: Boolean(isSel) });
|
|
1286
|
+
hit2.push({ id: n.id, x: p.x, y: p.y, r });
|
|
1287
|
+
}
|
|
1288
|
+
state.uni.hit = hit2;
|
|
1289
|
+
for (const e of reconsiderEdges) {
|
|
1290
|
+
const p1 = pos2.get(e.fromNodeId);
|
|
1291
|
+
const p2 = pos2.get(e.toNodeId);
|
|
1292
|
+
if (!p1 || !p2) continue;
|
|
1293
|
+
drawArrow(p1, p2, { dashed: true, alpha: 0.35 });
|
|
1294
|
+
const mx = (p1.x + p2.x) / 2;
|
|
1295
|
+
const my = (p1.y + p2.y) / 2;
|
|
1296
|
+
ctx.save();
|
|
1297
|
+
ctx.font = "11px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1298
|
+
ctx.fillStyle = rgbaFromCssVar("--muted", 0.95, "#6fd76f");
|
|
1299
|
+
ctx.fillText("reconsider", mx + 6, my + 14);
|
|
1300
|
+
ctx.restore();
|
|
1301
|
+
}
|
|
1302
|
+
for (const li of loopInfo) {
|
|
1303
|
+
const p = pos2.get(li.nodeId);
|
|
1304
|
+
if (!p) continue;
|
|
1305
|
+
const label = li.ran > 0 ? `retry x${li.ran}` : li.planned > 0 ? `fix-loop x${li.planned}` : "";
|
|
1306
|
+
drawSelfLoop(p, 12, label, li.ran > 0 ? 0.95 : 0.55);
|
|
1307
|
+
}
|
|
1308
|
+
$("inflPill").textContent = String(evs2.length || 0);
|
|
1309
|
+
$("inflBox").textContent = evs2.slice(-80).map((e) => JSON.stringify(e)).join("\n");
|
|
1310
|
+
ctx.restore();
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (!spec || !spec.root) {
|
|
1314
|
+
ctx.fillStyle = cssVar("--muted", "#6fd76f");
|
|
1315
|
+
ctx.font = "14px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1316
|
+
ctx.fillText("Universe not loaded.", 20, 30);
|
|
1317
|
+
ctx.restore();
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const nodes = flattenNodes(spec.root);
|
|
1321
|
+
const centerId = String(spec.root.id || "");
|
|
1322
|
+
const pos = layoutTree(nodes, centerId);
|
|
1323
|
+
state.uni.pos = pos;
|
|
1324
|
+
const treeEdges = [];
|
|
1325
|
+
for (const n of nodes) if (n.parentId) treeEdges.push({ from: n.parentId, to: n.id });
|
|
1326
|
+
const evs = contexts && Array.isArray(contexts.task) ? contexts.task : [];
|
|
1327
|
+
const agents = spec && Array.isArray(spec.agents) ? spec.agents : [];
|
|
1328
|
+
const agentToNode = new Map(agents.map((a) => [String(a.id || ""), String(a.nodeId || "")]));
|
|
1329
|
+
const a2a = [];
|
|
1330
|
+
for (const ev of evs) {
|
|
1331
|
+
const kind = String(ev.kind || "");
|
|
1332
|
+
if (kind === "a2a.envelope" || kind === "a2a.routed") {
|
|
1333
|
+
const fromAgent = String(ev.actorId || "");
|
|
1334
|
+
const toAgent = String((ev.payload || {}).toAgentId || "");
|
|
1335
|
+
const from = agentToNode.get(fromAgent) || "";
|
|
1336
|
+
const to = agentToNode.get(toAgent) || "";
|
|
1337
|
+
if (from && to) a2a.push({ from, to, fromAgent, toAgent, ts: String(ev.ts || ""), kind });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
state.uni.nodes = nodes;
|
|
1341
|
+
state.uni.edges = a2a;
|
|
1342
|
+
state.uni.seek = 0;
|
|
1343
|
+
$("uniSeek").max = String(Math.max(0, a2a.length));
|
|
1344
|
+
$("uniSeek").value = "0";
|
|
1345
|
+
$("inflPill").textContent = String(evs.length || 0);
|
|
1346
|
+
$("inflBox").textContent = evs.slice(-80).map((e) => JSON.stringify(e)).join("\n");
|
|
1347
|
+
ctx.strokeStyle = rgbaFromCssVar("--hot", 0.18, "#00ff66");
|
|
1348
|
+
ctx.lineWidth = 1;
|
|
1349
|
+
for (const e of treeEdges) {
|
|
1350
|
+
const p1 = pos.get(e.from);
|
|
1351
|
+
const p2 = pos.get(e.to);
|
|
1352
|
+
if (!p1 || !p2) continue;
|
|
1353
|
+
ctx.beginPath();
|
|
1354
|
+
ctx.moveTo(p1.x, p1.y);
|
|
1355
|
+
ctx.lineTo(p2.x, p2.y);
|
|
1356
|
+
ctx.stroke();
|
|
1357
|
+
}
|
|
1358
|
+
const hit = [];
|
|
1359
|
+
const usedLabels = [];
|
|
1360
|
+
ctx.font = "12px " + getComputedStyle(document.documentElement).getPropertyValue("--mono");
|
|
1361
|
+
for (const n of nodes) {
|
|
1362
|
+
const p = pos.get(n.id);
|
|
1363
|
+
if (!p) continue;
|
|
1364
|
+
const isRoot = n.id === centerId;
|
|
1365
|
+
const isSel = state.uni.selectedNodeId && String(state.uni.selectedNodeId) === String(n.id);
|
|
1366
|
+
const r = isRoot ? 14 : 10;
|
|
1367
|
+
ctx.fillStyle = cssVar("--canvasBg", "#030503");
|
|
1368
|
+
ctx.strokeStyle = cssVar("--hot", "#00ff66");
|
|
1369
|
+
ctx.lineWidth = isSel ? 4 : 2;
|
|
1370
|
+
if (isSel) {
|
|
1371
|
+
ctx.shadowColor = rgbaFromCssVar("--hot", 0.7, "#00ff66");
|
|
1372
|
+
ctx.shadowBlur = 14;
|
|
1373
|
+
} else {
|
|
1374
|
+
ctx.shadowColor = "transparent";
|
|
1375
|
+
ctx.shadowBlur = 0;
|
|
1376
|
+
}
|
|
1377
|
+
ctx.beginPath();
|
|
1378
|
+
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
1379
|
+
ctx.fill();
|
|
1380
|
+
ctx.stroke();
|
|
1381
|
+
if (isRoot) {
|
|
1382
|
+
ctx.beginPath();
|
|
1383
|
+
ctx.arc(p.x, p.y, r - 5, 0, Math.PI * 2);
|
|
1384
|
+
ctx.stroke();
|
|
1385
|
+
}
|
|
1386
|
+
const labelFill = cssVar("--uniLabel", cssVar("--fg", "#b7ffb7"));
|
|
1387
|
+
const labelStroke = cssVar("--uniLabelStroke", "transparent");
|
|
1388
|
+
ctx.fillStyle = labelFill;
|
|
1389
|
+
const baseLabel = n.name && n.id ? `${n.name} (${n.id})` : n.name || n.id;
|
|
1390
|
+
const label = pickLabel(baseLabel, isSel ? 64 : 36);
|
|
1391
|
+
const place = placeLabel(ctx, p.x, p.y, r, label, usedLabels);
|
|
1392
|
+
if (labelStroke && labelStroke !== "transparent") {
|
|
1393
|
+
ctx.save();
|
|
1394
|
+
ctx.strokeStyle = labelStroke;
|
|
1395
|
+
ctx.lineWidth = 3;
|
|
1396
|
+
ctx.lineJoin = "round";
|
|
1397
|
+
ctx.strokeText(label, place.tx, place.ty);
|
|
1398
|
+
ctx.restore();
|
|
1399
|
+
}
|
|
1400
|
+
ctx.fillText(label, place.tx, place.ty);
|
|
1401
|
+
hit.push({ id: n.id, x: p.x, y: p.y, r: r + 6 });
|
|
1402
|
+
}
|
|
1403
|
+
state.uni.hit = hit;
|
|
1404
|
+
drawReplayEdges(ctx, pos, a2a, 0);
|
|
1405
|
+
ctx.restore();
|
|
1406
|
+
}
|
|
1407
|
+
function updateUniverseReplay() {
|
|
1408
|
+
drawUniverse(state.uni.spec, state.uni.contexts);
|
|
1409
|
+
const canvas = $("uniCanvas");
|
|
1410
|
+
const ctx = canvas.getContext("2d");
|
|
1411
|
+
if (!ctx) return;
|
|
1412
|
+
ctx.save();
|
|
1413
|
+
applyUniverseTransform(ctx);
|
|
1414
|
+
drawReplayEdges(ctx, state.uni.pos, state.uni.edges, state.uni.seek);
|
|
1415
|
+
ctx.restore();
|
|
1416
|
+
}
|
|
1417
|
+
function renderNodeDetail(nodeId) {
|
|
1418
|
+
const n = (state.uni.nodes || []).find((x) => x.id === nodeId);
|
|
1419
|
+
if (!n) {
|
|
1420
|
+
$("nodeDetail").textContent = "(none)";
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const isFlowSpec = Boolean(state.uni.spec && String(state.uni.spec.schemaVersion || "") === "maria_lite_universe_flow_gui_spec_v1");
|
|
1424
|
+
const agents = !isFlowSpec && state.uni.spec && Array.isArray(state.uni.spec.agents) ? state.uni.spec.agents.filter((a) => String(a.nodeId || "") === nodeId) : [];
|
|
1425
|
+
const topic = n.topic ? decodeVisibleNewlines(n.topic) : "";
|
|
1426
|
+
const html = [];
|
|
1427
|
+
html.push(`<div class="item"><div class="k">node</div><div class="v"><b>${escapeHtml(n.name || "(no name)")}</b> <span class="pill">${escapeHtml(n.id)}</span></div></div>`);
|
|
1428
|
+
html.push(`<div class="item"><div class="k">level</div><div class="v">${escapeHtml(n.level || "")}</div></div>`);
|
|
1429
|
+
if (isFlowSpec && n.flow) {
|
|
1430
|
+
const f = n.flow || {};
|
|
1431
|
+
html.push(`<div class="item"><div class="k">lane</div><div class="v">${escapeHtml(String(f.lane || ""))}</div></div>`);
|
|
1432
|
+
html.push(`<div class="item"><div class="k">type</div><div class="v">${escapeHtml(String(f.type || ""))}</div></div>`);
|
|
1433
|
+
if (String(f.actionId || "")) html.push(`<div class="item"><div class="k">actionId</div><div class="v">${escapeHtml(String(f.actionId || ""))}</div></div>`);
|
|
1434
|
+
if (String(f.cmd || "")) html.push(`<div class="item"><div class="k">cmd</div><div class="v">${escapeHtml(String(f.cmd || ""))}</div></div>`);
|
|
1435
|
+
if (typeof f.hasWhen === "boolean") html.push(`<div class="item"><div class="k">when</div><div class="v">${escapeHtml(f.hasWhen ? "yes" : "no")}</div></div>`);
|
|
1436
|
+
if (typeof f.hasChecks === "boolean") html.push(`<div class="item"><div class="k">checks</div><div class="v">${escapeHtml(f.hasChecks ? "yes" : "no")}</div></div>`);
|
|
1437
|
+
if (typeof f.maxFixAttempts === "number")
|
|
1438
|
+
html.push(`<div class="item"><div class="k">fix</div><div class="v">${escapeHtml(String(f.maxFixAttempts || 0))} attempts</div></div>`);
|
|
1439
|
+
if (Array.isArray(f.reconsiderStepIds) && f.reconsiderStepIds.length) {
|
|
1440
|
+
html.push(
|
|
1441
|
+
`<div class="item"><div class="k">reconsider</div><div class="v">${escapeHtml(
|
|
1442
|
+
String(f.reconsiderStepIds.slice(0, 8).join(", "))
|
|
1443
|
+
)}</div></div>`
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
const parN = state.uni.flowMeta && state.uni.flowMeta.bucketSizeById ? state.uni.flowMeta.bucketSizeById.get(String(n.id)) ?? 1 : 1;
|
|
1447
|
+
if (Number(parN) > 1) {
|
|
1448
|
+
html.push(`<div class="item"><div class="k">parallel</div><div class="v">${escapeHtml(`stage with ${Number(parN)} nodes`)}</div></div>`);
|
|
1449
|
+
}
|
|
1450
|
+
html.push(
|
|
1451
|
+
`<div class="item"><div class="k">marks</div><div class="v">${escapeHtml(
|
|
1452
|
+
`${n.isStart ? "start " : ""}${n.isEnd ? "end" : ""}`.trim() || "(none)"
|
|
1453
|
+
)}</div></div>`
|
|
1454
|
+
);
|
|
1455
|
+
html.push(`<div class="item"><div class="k">context</div><div class="v"><div id="nodeCtx">(loading)</div></div></div>`);
|
|
1456
|
+
}
|
|
1457
|
+
if (topic) {
|
|
1458
|
+
html.push(`<div class="item"><div class="k">topic</div><div class="v">${escapeHtml(topic)}</div></div>`);
|
|
1459
|
+
}
|
|
1460
|
+
if (agents.length) {
|
|
1461
|
+
const lines = agents.slice(0, 60).map((a) => `- ${String(a.id || "")} :: ${String(a.name || "")} (${String(a.kind || "")}) :: ${String(a.role || "")}`);
|
|
1462
|
+
html.push(`<div class="item"><div class="k">agents (${agents.length})</div><div class="v">${escapeHtml(lines.join("\n"))}</div></div>`);
|
|
1463
|
+
}
|
|
1464
|
+
$("nodeDetail").innerHTML = html.join('<div style="height:8px"></div>');
|
|
1465
|
+
if (isFlowSpec && state.uni.spec && n.flow) {
|
|
1466
|
+
const f = n.flow || {};
|
|
1467
|
+
const actionId = String(f.actionId || "").trim();
|
|
1468
|
+
const nodeId2 = String(n.id || "").trim();
|
|
1469
|
+
const manifest = state.uni.spec?.manifest;
|
|
1470
|
+
const rootDirRel = manifest && typeof manifest.rootDirRel === "string" ? String(manifest.rootDirRel).trim() : "";
|
|
1471
|
+
const ctxBox = document.getElementById("nodeCtx");
|
|
1472
|
+
if (ctxBox && rootDirRel) {
|
|
1473
|
+
const candidates = [
|
|
1474
|
+
actionId ? `${rootDirRel}/contexts/steps/${actionId}.md` : "",
|
|
1475
|
+
actionId ? `${rootDirRel}/contexts/${actionId}.md` : "",
|
|
1476
|
+
nodeId2 ? `${rootDirRel}/contexts/nodes/${nodeId2}.md` : "",
|
|
1477
|
+
actionId ? `${rootDirRel}/contexts/steps/${actionId}.txt` : ""
|
|
1478
|
+
].filter(Boolean);
|
|
1479
|
+
const key = candidates.join("|");
|
|
1480
|
+
ctxBox.dataset.key = key;
|
|
1481
|
+
(async () => {
|
|
1482
|
+
for (const rel of candidates) {
|
|
1483
|
+
const txt = await getMarkdownCached(rel);
|
|
1484
|
+
if (!txt.trim()) continue;
|
|
1485
|
+
const cur2 = document.getElementById("nodeCtx");
|
|
1486
|
+
if (!cur2) return;
|
|
1487
|
+
if (String(cur2.dataset.key || "") !== key) return;
|
|
1488
|
+
cur2.innerHTML = `<div class="md">${mdToHtml(txt)}</div><div style="height:6px"></div><div class="pill">${escapeHtml(rel)}</div>`;
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const cur = document.getElementById("nodeCtx");
|
|
1492
|
+
if (!cur) return;
|
|
1493
|
+
if (String(cur.dataset.key || "") !== key) return;
|
|
1494
|
+
cur.textContent = "(none)";
|
|
1495
|
+
})().catch(() => {
|
|
1496
|
+
const cur = document.getElementById("nodeCtx");
|
|
1497
|
+
if (!cur) return;
|
|
1498
|
+
if (String(cur.dataset.key || "") !== key) return;
|
|
1499
|
+
cur.textContent = "(failed)";
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function attachUniverseInteractions() {
|
|
1505
|
+
const canvas = $("uniCanvas");
|
|
1506
|
+
canvas.addEventListener(
|
|
1507
|
+
"wheel",
|
|
1508
|
+
(e) => {
|
|
1509
|
+
e.preventDefault();
|
|
1510
|
+
const rect = canvas.getBoundingClientRect();
|
|
1511
|
+
const sx = (e.clientX - rect.left) * (canvas.width / rect.width);
|
|
1512
|
+
const sy = (e.clientY - rect.top) * (canvas.height / rect.height);
|
|
1513
|
+
const before = screenToWorld(sx, sy);
|
|
1514
|
+
const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
|
|
1515
|
+
state.uni.view.scale = Math.max(0.35, Math.min(3.5, state.uni.view.scale * factor));
|
|
1516
|
+
state.uni.view.tx = sx - before.x * state.uni.view.scale;
|
|
1517
|
+
state.uni.view.ty = sy - before.y * state.uni.view.scale;
|
|
1518
|
+
updateUniverseReplay();
|
|
1519
|
+
},
|
|
1520
|
+
{ passive: false }
|
|
1521
|
+
);
|
|
1522
|
+
canvas.addEventListener("mousedown", (e) => {
|
|
1523
|
+
state.uni.view.panning = true;
|
|
1524
|
+
state.uni.view.panX = e.clientX;
|
|
1525
|
+
state.uni.view.panY = e.clientY;
|
|
1526
|
+
});
|
|
1527
|
+
window.addEventListener("mousemove", (e) => {
|
|
1528
|
+
if (!state.uni.view.panning) return;
|
|
1529
|
+
const dx = e.clientX - state.uni.view.panX;
|
|
1530
|
+
const dy = e.clientY - state.uni.view.panY;
|
|
1531
|
+
state.uni.view.panX = e.clientX;
|
|
1532
|
+
state.uni.view.panY = e.clientY;
|
|
1533
|
+
state.uni.view.tx += dx;
|
|
1534
|
+
state.uni.view.ty += dy;
|
|
1535
|
+
updateUniverseReplay();
|
|
1536
|
+
});
|
|
1537
|
+
window.addEventListener("mouseup", () => {
|
|
1538
|
+
state.uni.view.panning = false;
|
|
1539
|
+
});
|
|
1540
|
+
const dist = (a, b) => {
|
|
1541
|
+
const dx = a.clientX - b.clientX;
|
|
1542
|
+
const dy = a.clientY - b.clientY;
|
|
1543
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1544
|
+
};
|
|
1545
|
+
canvas.addEventListener(
|
|
1546
|
+
"touchstart",
|
|
1547
|
+
(e) => {
|
|
1548
|
+
if (e.touches.length === 1) {
|
|
1549
|
+
state.uni.view.panning = true;
|
|
1550
|
+
state.uni.view.panX = e.touches[0].clientX;
|
|
1551
|
+
state.uni.view.panY = e.touches[0].clientY;
|
|
1552
|
+
}
|
|
1553
|
+
if (e.touches.length === 2) {
|
|
1554
|
+
state.uni.view.panning = false;
|
|
1555
|
+
state.uni.view.pinchDist = dist(e.touches[0], e.touches[1]);
|
|
1556
|
+
state.uni.view.pinchScale = state.uni.view.scale;
|
|
1557
|
+
}
|
|
1558
|
+
},
|
|
1559
|
+
{ passive: true }
|
|
1560
|
+
);
|
|
1561
|
+
canvas.addEventListener(
|
|
1562
|
+
"touchmove",
|
|
1563
|
+
(e) => {
|
|
1564
|
+
if (e.touches.length === 1 && state.uni.view.panning) {
|
|
1565
|
+
const dx = e.touches[0].clientX - state.uni.view.panX;
|
|
1566
|
+
const dy = e.touches[0].clientY - state.uni.view.panY;
|
|
1567
|
+
state.uni.view.panX = e.touches[0].clientX;
|
|
1568
|
+
state.uni.view.panY = e.touches[0].clientY;
|
|
1569
|
+
state.uni.view.tx += dx;
|
|
1570
|
+
state.uni.view.ty += dy;
|
|
1571
|
+
updateUniverseReplay();
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (e.touches.length === 2) {
|
|
1575
|
+
e.preventDefault();
|
|
1576
|
+
const d = dist(e.touches[0], e.touches[1]) || 1;
|
|
1577
|
+
const rect = canvas.getBoundingClientRect();
|
|
1578
|
+
const mx = ((e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left) * (canvas.width / rect.width);
|
|
1579
|
+
const my = ((e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top) * (canvas.height / rect.height);
|
|
1580
|
+
const before = screenToWorld(mx, my);
|
|
1581
|
+
const factor = d / (state.uni.view.pinchDist || d);
|
|
1582
|
+
state.uni.view.scale = Math.max(0.35, Math.min(3.5, state.uni.view.pinchScale * factor));
|
|
1583
|
+
state.uni.view.tx = mx - before.x * state.uni.view.scale;
|
|
1584
|
+
state.uni.view.ty = my - before.y * state.uni.view.scale;
|
|
1585
|
+
updateUniverseReplay();
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
{ passive: false }
|
|
1589
|
+
);
|
|
1590
|
+
canvas.addEventListener(
|
|
1591
|
+
"touchend",
|
|
1592
|
+
() => {
|
|
1593
|
+
state.uni.view.panning = false;
|
|
1594
|
+
state.uni.view.pinchDist = 0;
|
|
1595
|
+
},
|
|
1596
|
+
{ passive: true }
|
|
1597
|
+
);
|
|
1598
|
+
canvas.addEventListener("click", (e) => {
|
|
1599
|
+
const rect = canvas.getBoundingClientRect();
|
|
1600
|
+
const sx = (e.clientX - rect.left) * (canvas.width / rect.width);
|
|
1601
|
+
const sy = (e.clientY - rect.top) * (canvas.height / rect.height);
|
|
1602
|
+
const w = screenToWorld(sx, sy);
|
|
1603
|
+
for (const h of state.uni.hit || []) {
|
|
1604
|
+
const dx = w.x - h.x;
|
|
1605
|
+
const dy = w.y - h.y;
|
|
1606
|
+
if (Math.sqrt(dx * dx + dy * dy) <= h.r) {
|
|
1607
|
+
state.uni.selectedNodeId = h.id;
|
|
1608
|
+
renderNodeDetail(h.id);
|
|
1609
|
+
updateUniverseReplay();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
function extractNdcCodes(topic) {
|
|
1616
|
+
const t = String(topic ?? "");
|
|
1617
|
+
const codes = /* @__PURE__ */ new Set();
|
|
1618
|
+
const ndcBlock = /\bNDC\b([^\n]{0,80})/gi;
|
|
1619
|
+
let m;
|
|
1620
|
+
while (m = ndcBlock.exec(t)) {
|
|
1621
|
+
const frag = String(m[1] || "");
|
|
1622
|
+
const nums = frag.match(/\b\d{3}\b/g) || [];
|
|
1623
|
+
for (const n of nums) codes.add(n);
|
|
1624
|
+
const slash = frag.match(/\b\d{3}(?:\/\d{3})+\b/g) || [];
|
|
1625
|
+
for (const s of slash) for (const n of s.split("/")) codes.add(n);
|
|
1626
|
+
}
|
|
1627
|
+
const par = t.match(/\(\s*\d{3}(?:\s*\/\s*\d{3})*\s*\)/g) || [];
|
|
1628
|
+
for (const p of par) for (const n of p.match(/\b\d{3}\b/g) || []) codes.add(n);
|
|
1629
|
+
const slashAnywhere = t.match(/\b\d{3}\s*\/\s*\d{3}(?:\s*\/\s*\d{3})*/g) || [];
|
|
1630
|
+
for (const s of slashAnywhere) for (const n of s.match(/\b\d{3}\b/g) || []) codes.add(n);
|
|
1631
|
+
return Array.from(codes).sort();
|
|
1632
|
+
}
|
|
1633
|
+
function renderMultiverseTree() {
|
|
1634
|
+
const q = String($("mvQuery").value || "").trim().toLowerCase();
|
|
1635
|
+
const items = (state.multiverse || []).filter((u) => {
|
|
1636
|
+
const s = String((u.universeName || "") + " " + (u.topic || "")).toLowerCase();
|
|
1637
|
+
if (!q) return true;
|
|
1638
|
+
const codes = Array.isArray(u.ndc3s) ? u.ndc3s : extractNdcCodes(u.topic);
|
|
1639
|
+
return s.includes(q) || codes.some((c) => String(c).includes(q));
|
|
1640
|
+
});
|
|
1641
|
+
const majors = /* @__PURE__ */ new Map();
|
|
1642
|
+
for (const u of items) {
|
|
1643
|
+
const codes = Array.isArray(u.ndc3s) ? u.ndc3s : extractNdcCodes(u.topic);
|
|
1644
|
+
if (!codes.length) {
|
|
1645
|
+
const m = majors.get("other") || /* @__PURE__ */ new Map();
|
|
1646
|
+
const arr = m.get("other") || [];
|
|
1647
|
+
arr.push(u);
|
|
1648
|
+
m.set("other", arr);
|
|
1649
|
+
majors.set("other", m);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
for (const code of codes) {
|
|
1653
|
+
const maj = String(code || "")[0] || "other";
|
|
1654
|
+
const m = majors.get(maj) || /* @__PURE__ */ new Map();
|
|
1655
|
+
const arr = m.get(code) || [];
|
|
1656
|
+
arr.push(u);
|
|
1657
|
+
m.set(code, arr);
|
|
1658
|
+
majors.set(maj, m);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const majorKeys = Array.from(majors.keys()).sort((a, b) => a === "other" ? 1 : b === "other" ? -1 : Number(a) - Number(b));
|
|
1662
|
+
const box = $("mvTree");
|
|
1663
|
+
box.innerHTML = "";
|
|
1664
|
+
for (const maj of majorKeys) {
|
|
1665
|
+
const m = majors.get(maj);
|
|
1666
|
+
const legend = state.mvLegend || null;
|
|
1667
|
+
const label = maj === "other" ? "other" : `NDC ${maj}xx${legend && legend[maj] ? " \u2014 " + legend[maj] : ""}`;
|
|
1668
|
+
let count = 0;
|
|
1669
|
+
for (const arr of m.values()) count += arr.length;
|
|
1670
|
+
const hd = document.createElement("div");
|
|
1671
|
+
hd.className = "item";
|
|
1672
|
+
const open = state.nav.mvOpenMajor.has(maj);
|
|
1673
|
+
hd.innerHTML = `<div class="k">${escapeHtml(label)} <span class="pill">${count}</span></div><div class="v">${open ? "\u25BC" : "\u25B6"}</div>`;
|
|
1674
|
+
hd.addEventListener("click", () => {
|
|
1675
|
+
if (state.nav.mvOpenMajor.has(maj)) state.nav.mvOpenMajor.delete(maj);
|
|
1676
|
+
else state.nav.mvOpenMajor.add(maj);
|
|
1677
|
+
renderMultiverseTree();
|
|
1678
|
+
});
|
|
1679
|
+
box.appendChild(hd);
|
|
1680
|
+
if (!open) continue;
|
|
1681
|
+
const codes = Array.from(m.keys()).sort((a, b) => String(a).localeCompare(String(b)));
|
|
1682
|
+
for (const code of codes) {
|
|
1683
|
+
const arr0 = m.get(code) || [];
|
|
1684
|
+
const uniq = /* @__PURE__ */ new Map();
|
|
1685
|
+
for (const u of arr0) uniq.set(String(u.universeName || ""), u);
|
|
1686
|
+
const arr = Array.from(uniq.values());
|
|
1687
|
+
arr.sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")) || String(a.universeName || "").localeCompare(String(b.universeName || "")));
|
|
1688
|
+
const openCodeKey = `${maj}:${code}`;
|
|
1689
|
+
const openCode = state.nav.mvOpenCode.has(openCodeKey);
|
|
1690
|
+
const ch = document.createElement("div");
|
|
1691
|
+
ch.className = "item";
|
|
1692
|
+
ch.innerHTML = `<div class="k">${escapeHtml(code)} <span class="pill">${arr.length}</span></div><div class="v">${openCode ? "\u25BC" : "\u25B6"}</div>`;
|
|
1693
|
+
ch.addEventListener("click", () => {
|
|
1694
|
+
if (state.nav.mvOpenCode.has(openCodeKey)) state.nav.mvOpenCode.delete(openCodeKey);
|
|
1695
|
+
else state.nav.mvOpenCode.add(openCodeKey);
|
|
1696
|
+
renderMultiverseTree();
|
|
1697
|
+
});
|
|
1698
|
+
box.appendChild(ch);
|
|
1699
|
+
if (!openCode) continue;
|
|
1700
|
+
for (const u of arr.slice(0, 120)) {
|
|
1701
|
+
const it = document.createElement("div");
|
|
1702
|
+
it.className = "item";
|
|
1703
|
+
it.innerHTML = `<div class="k">[${escapeHtml(u.kind)}] ${escapeHtml(u.universeName)}</div><div class="v">topic=${escapeHtml(u.topic)}
|
|
1704
|
+
createdAt=${escapeHtml(u.createdAt)}
|
|
1705
|
+
root=${escapeHtml(u.rootDirRel || "")}</div>`;
|
|
1706
|
+
it.addEventListener("click", () => {
|
|
1707
|
+
$("uniKind").value = String(u.kind);
|
|
1708
|
+
$("uniName").value = String(u.universeName);
|
|
1709
|
+
setTab("universe");
|
|
1710
|
+
$("uniLoad").click();
|
|
1711
|
+
});
|
|
1712
|
+
box.appendChild(it);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
async function loadMultiverse() {
|
|
1718
|
+
const mv = await api("/api/multiverse");
|
|
1719
|
+
state.multiverse = mv.items || [];
|
|
1720
|
+
state.mvLegend = mv && mv.legend || null;
|
|
1721
|
+
$("mvPill").textContent = String(state.multiverse.length);
|
|
1722
|
+
renderMultiverseTree();
|
|
1723
|
+
}
|
|
1724
|
+
function ndcFromKpPath(rel) {
|
|
1725
|
+
const parts = String(rel || "").split("/").filter(Boolean);
|
|
1726
|
+
const ndcIdx = parts.indexOf("ndc");
|
|
1727
|
+
if (ndcIdx < 0) return null;
|
|
1728
|
+
const code = parts[ndcIdx + 2] || "";
|
|
1729
|
+
const maj = parts[ndcIdx + 1] || "";
|
|
1730
|
+
const c = /^\d{3}$/.test(maj) ? maj : /^\d{3}$/.test(code) ? code : "";
|
|
1731
|
+
return c || null;
|
|
1732
|
+
}
|
|
1733
|
+
function renderKpNdcTree(items) {
|
|
1734
|
+
const majors = /* @__PURE__ */ new Map();
|
|
1735
|
+
for (const p of items) {
|
|
1736
|
+
const code = ndcFromKpPath(p.rel);
|
|
1737
|
+
const key = code || "other";
|
|
1738
|
+
const maj = key === "other" ? "other" : key[0];
|
|
1739
|
+
const m = majors.get(maj) || /* @__PURE__ */ new Map();
|
|
1740
|
+
const arr = m.get(key) || [];
|
|
1741
|
+
arr.push(p.rel);
|
|
1742
|
+
m.set(key, arr);
|
|
1743
|
+
majors.set(maj, m);
|
|
1744
|
+
}
|
|
1745
|
+
const majorKeys = Array.from(majors.keys()).sort((a, b) => a === "other" ? 1 : b === "other" ? -1 : Number(a) - Number(b));
|
|
1746
|
+
const box = $("kpList");
|
|
1747
|
+
box.innerHTML = "";
|
|
1748
|
+
for (const maj of majorKeys) {
|
|
1749
|
+
const m = majors.get(maj);
|
|
1750
|
+
const label = maj === "other" ? "other" : `NDC ${maj}xx`;
|
|
1751
|
+
let count = 0;
|
|
1752
|
+
for (const arr of m.values()) count += arr.length;
|
|
1753
|
+
const hd = document.createElement("div");
|
|
1754
|
+
hd.className = "item";
|
|
1755
|
+
const open = state.nav.kpOpenMajor.has(maj);
|
|
1756
|
+
hd.innerHTML = `<div class="k">${escapeHtml(label)} <span class="pill">${count}</span></div><div class="v">${open ? "\u25BC" : "\u25B6"}</div>`;
|
|
1757
|
+
hd.addEventListener("click", () => {
|
|
1758
|
+
if (state.nav.kpOpenMajor.has(maj)) state.nav.kpOpenMajor.delete(maj);
|
|
1759
|
+
else state.nav.kpOpenMajor.add(maj);
|
|
1760
|
+
renderKpNdcTree(items);
|
|
1761
|
+
});
|
|
1762
|
+
box.appendChild(hd);
|
|
1763
|
+
if (!open) continue;
|
|
1764
|
+
const codes = Array.from(m.keys()).sort((a, b) => String(a).localeCompare(String(b)));
|
|
1765
|
+
for (const code of codes) {
|
|
1766
|
+
const rels = (m.get(code) || []).slice().sort((a, b) => a.localeCompare(b));
|
|
1767
|
+
const openCodeKey = `${maj}:${code}`;
|
|
1768
|
+
const openCode = state.nav.kpOpenCode.has(openCodeKey);
|
|
1769
|
+
const ch = document.createElement("div");
|
|
1770
|
+
ch.className = "item";
|
|
1771
|
+
ch.innerHTML = `<div class="k">${escapeHtml(code)} <span class="pill">${rels.length}</span></div><div class="v">${openCode ? "\u25BC" : "\u25B6"}</div>`;
|
|
1772
|
+
ch.addEventListener("click", () => {
|
|
1773
|
+
if (state.nav.kpOpenCode.has(openCodeKey)) state.nav.kpOpenCode.delete(openCodeKey);
|
|
1774
|
+
else state.nav.kpOpenCode.add(openCodeKey);
|
|
1775
|
+
renderKpNdcTree(items);
|
|
1776
|
+
});
|
|
1777
|
+
box.appendChild(ch);
|
|
1778
|
+
if (!openCode) continue;
|
|
1779
|
+
for (const rel of rels.slice(0, 200)) {
|
|
1780
|
+
const it = document.createElement("div");
|
|
1781
|
+
it.className = "item";
|
|
1782
|
+
it.innerHTML = `<div class="k">yaml</div><div class="v">${escapeHtml(rel)}</div>`;
|
|
1783
|
+
it.addEventListener("click", async () => {
|
|
1784
|
+
const file = await api(`/api/file/text?path=${encodeURIComponent(rel)}&max=400000`);
|
|
1785
|
+
$("kpViewPill").textContent = rel.split("/").slice(-1)[0] || "yaml";
|
|
1786
|
+
$("kpViewer").textContent = typeof file?.text === "string" ? file.text : "";
|
|
1787
|
+
});
|
|
1788
|
+
box.appendChild(it);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function jsonToYaml(v, indent = 0) {
|
|
1794
|
+
const pad = " ".repeat(Math.max(0, indent));
|
|
1795
|
+
if (v === null || v === void 0) return "null";
|
|
1796
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
1797
|
+
if (typeof v === "string") {
|
|
1798
|
+
const s = decodeVisibleNewlines(v);
|
|
1799
|
+
if (s.includes("\n")) {
|
|
1800
|
+
return "|\n" + s.split("\n").map((l) => pad + " " + l).join("\n");
|
|
1801
|
+
}
|
|
1802
|
+
if (s === "" || /[:\[\]{}#,]|^\s|\s$/.test(s)) return JSON.stringify(s);
|
|
1803
|
+
return s;
|
|
1804
|
+
}
|
|
1805
|
+
if (Array.isArray(v)) {
|
|
1806
|
+
if (!v.length) return "[]";
|
|
1807
|
+
return "\n" + v.map((x) => {
|
|
1808
|
+
const yy = jsonToYaml(x, indent + 1);
|
|
1809
|
+
if (typeof x === "object" && x !== null && !Array.isArray(x)) return pad + "- \n" + yy;
|
|
1810
|
+
return pad + "- " + yy;
|
|
1811
|
+
}).join("\n");
|
|
1812
|
+
}
|
|
1813
|
+
if (typeof v === "object") {
|
|
1814
|
+
const obj = v;
|
|
1815
|
+
const keys = Object.keys(obj);
|
|
1816
|
+
if (!keys.length) return "{}";
|
|
1817
|
+
const lines = [];
|
|
1818
|
+
for (const k of keys) {
|
|
1819
|
+
const vv = obj[k];
|
|
1820
|
+
if (typeof vv === "object" && vv !== null) {
|
|
1821
|
+
lines.push(`${pad}${k}:` + jsonToYaml(vv, indent + 1));
|
|
1822
|
+
} else {
|
|
1823
|
+
lines.push(`${pad}${k}: ${jsonToYaml(vv, 0)}`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return "\n" + lines.join("\n");
|
|
1827
|
+
}
|
|
1828
|
+
return JSON.stringify(v);
|
|
1829
|
+
}
|
|
1830
|
+
function kpLeafKey(ndc3, sub) {
|
|
1831
|
+
return `${ndc3}:${sub || ""}`;
|
|
1832
|
+
}
|
|
1833
|
+
async function loadKpLeaf(ndc3, sub) {
|
|
1834
|
+
const key = kpLeafKey(ndc3, sub);
|
|
1835
|
+
if (state.kpLeafItems.has(key)) return;
|
|
1836
|
+
const url = `/api/kp/ndc/leaf?ndc3=${encodeURIComponent(ndc3)}${sub ? `&sub=${encodeURIComponent(sub)}` : ""}&limit=200`;
|
|
1837
|
+
const out = await api(url);
|
|
1838
|
+
const items = out.items || [];
|
|
1839
|
+
state.kpLeafItems.set(key, items);
|
|
1840
|
+
}
|
|
1841
|
+
function renderKpNdcIndex() {
|
|
1842
|
+
const meta = state.kpNdcMeta || null;
|
|
1843
|
+
const legend = state.kpLegend || null;
|
|
1844
|
+
const leaves = Array.isArray(meta?.leaves) ? meta.leaves : [];
|
|
1845
|
+
const box = $("kpList");
|
|
1846
|
+
box.innerHTML = "";
|
|
1847
|
+
if (!leaves.length) {
|
|
1848
|
+
box.textContent = "(no ndc index)";
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const byMajor = /* @__PURE__ */ new Map();
|
|
1852
|
+
for (const leaf of leaves) {
|
|
1853
|
+
const ndc3 = String(leaf?.ndc3 || "").trim();
|
|
1854
|
+
if (!/^\d{3}$/.test(ndc3)) continue;
|
|
1855
|
+
const sub = typeof leaf?.sub === "string" ? String(leaf.sub).trim() : "";
|
|
1856
|
+
const maj = ndc3[0];
|
|
1857
|
+
const m = byMajor.get(maj) || /* @__PURE__ */ new Map();
|
|
1858
|
+
const arr = m.get(ndc3) || [];
|
|
1859
|
+
arr.push({ ndc3, sub: sub || null, recordCount: Number(leaf?.recordCount || leaf?.textExtractedCount || 0) || 0 });
|
|
1860
|
+
m.set(ndc3, arr);
|
|
1861
|
+
byMajor.set(maj, m);
|
|
1862
|
+
}
|
|
1863
|
+
const majorKeys = Array.from(byMajor.keys()).sort((a, b) => Number(a) - Number(b));
|
|
1864
|
+
for (const maj of majorKeys) {
|
|
1865
|
+
const m = byMajor.get(maj);
|
|
1866
|
+
let count = 0;
|
|
1867
|
+
for (const arr of m.values()) count += arr.length;
|
|
1868
|
+
const hd = document.createElement("div");
|
|
1869
|
+
hd.className = "item";
|
|
1870
|
+
const open = state.nav.kpOpenMajor.has(maj);
|
|
1871
|
+
hd.innerHTML = `<div class="k">NDC ${escapeHtml(maj)}xx${legend && legend[maj] ? " \u2014 " + escapeHtml(legend[maj]) : ""} <span class="pill">${count}</span></div><div class="v">${open ? "\u25BC" : "\u25B6"}</div>`;
|
|
1872
|
+
hd.addEventListener("click", () => {
|
|
1873
|
+
if (state.nav.kpOpenMajor.has(maj)) state.nav.kpOpenMajor.delete(maj);
|
|
1874
|
+
else state.nav.kpOpenMajor.add(maj);
|
|
1875
|
+
renderKpNdcIndex();
|
|
1876
|
+
});
|
|
1877
|
+
box.appendChild(hd);
|
|
1878
|
+
if (!open) continue;
|
|
1879
|
+
const codes = Array.from(m.keys()).sort((a, b) => String(a).localeCompare(String(b)));
|
|
1880
|
+
for (const ndc3 of codes) {
|
|
1881
|
+
const subs = (m.get(ndc3) || []).slice().sort((a, b) => String(a.sub || "").localeCompare(String(b.sub || "")));
|
|
1882
|
+
const openCodeKey = `${maj}:${ndc3}`;
|
|
1883
|
+
const openCode = state.nav.kpOpenCode.has(openCodeKey);
|
|
1884
|
+
const ch = document.createElement("div");
|
|
1885
|
+
ch.className = "item";
|
|
1886
|
+
ch.innerHTML = `<div class="k">${escapeHtml(ndc3)} <span class="pill">${subs.length}</span></div><div class="v">${openCode ? "\u25BC" : "\u25B6"}</div>`;
|
|
1887
|
+
ch.addEventListener("click", () => {
|
|
1888
|
+
if (state.nav.kpOpenCode.has(openCodeKey)) state.nav.kpOpenCode.delete(openCodeKey);
|
|
1889
|
+
else state.nav.kpOpenCode.add(openCodeKey);
|
|
1890
|
+
renderKpNdcIndex();
|
|
1891
|
+
});
|
|
1892
|
+
box.appendChild(ch);
|
|
1893
|
+
if (!openCode) continue;
|
|
1894
|
+
for (const leaf of subs) {
|
|
1895
|
+
const sub = leaf.sub;
|
|
1896
|
+
const leafKey = kpLeafKey(ndc3, sub);
|
|
1897
|
+
const has = state.kpLeafItems.has(leafKey);
|
|
1898
|
+
const leafOpenKey = `${ndc3}:${sub || ""}`;
|
|
1899
|
+
const leafOpen = state.nav.kpOpenCode.has(leafOpenKey);
|
|
1900
|
+
const lh = document.createElement("div");
|
|
1901
|
+
lh.className = "item";
|
|
1902
|
+
lh.innerHTML = `<div class="k">${escapeHtml(sub ? `${ndc3}/${sub}` : ndc3)} <span class="pill">${escapeHtml(String(leaf.recordCount || 0))}</span></div><div class="v">${leafOpen ? "\u25BC" : "\u25B6"}</div>`;
|
|
1903
|
+
lh.addEventListener("click", () => {
|
|
1904
|
+
if (state.nav.kpOpenCode.has(leafOpenKey)) state.nav.kpOpenCode.delete(leafOpenKey);
|
|
1905
|
+
else {
|
|
1906
|
+
state.nav.kpOpenCode.add(leafOpenKey);
|
|
1907
|
+
if (!has) void loadKpLeaf(ndc3, sub).then(() => renderKpNdcIndex());
|
|
1908
|
+
}
|
|
1909
|
+
renderKpNdcIndex();
|
|
1910
|
+
});
|
|
1911
|
+
box.appendChild(lh);
|
|
1912
|
+
if (!leafOpen) continue;
|
|
1913
|
+
const items = state.kpLeafItems.get(leafKey) || [];
|
|
1914
|
+
for (const rec of items.slice(0, 120)) {
|
|
1915
|
+
const title = String(rec.entryTitle || rec.packTitle || rec.entryId || "");
|
|
1916
|
+
const it = document.createElement("div");
|
|
1917
|
+
it.className = "item";
|
|
1918
|
+
it.innerHTML = `<div class="k">${escapeHtml(title)}</div><div class="v">packId=${escapeHtml(rec.packId || "")}
|
|
1919
|
+
entryId=${escapeHtml(rec.entryId || "")}
|
|
1920
|
+
ndc3=${escapeHtml(rec.ndc3 || "")}</div>`;
|
|
1921
|
+
it.addEventListener("click", () => {
|
|
1922
|
+
$("kpViewPill").textContent = String(rec.entryId || "yaml");
|
|
1923
|
+
$("kpViewer").textContent = jsonToYaml(rec);
|
|
1924
|
+
});
|
|
1925
|
+
box.appendChild(it);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
async function loadKp() {
|
|
1932
|
+
const modeEl = document.getElementById("kpMode");
|
|
1933
|
+
const mode = modeEl ? String(modeEl.value || "ndc") : "ndc";
|
|
1934
|
+
const rootEl = document.getElementById("kpRoot");
|
|
1935
|
+
if (rootEl) rootEl.disabled = mode !== "files";
|
|
1936
|
+
if (mode === "ndc") {
|
|
1937
|
+
const out2 = await api("/api/kp/ndc/meta");
|
|
1938
|
+
state.kpNdcMeta = out2.meta || null;
|
|
1939
|
+
state.kpLegend = out2 && out2.legend || null;
|
|
1940
|
+
$("kpPill").textContent = String(state.kpNdcMeta && (state.kpNdcMeta.packCount || state.kpNdcMeta.totalRecords) || 0);
|
|
1941
|
+
$("kpListPill").textContent = "ndc";
|
|
1942
|
+
renderKpNdcIndex();
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
const root = $("kpRoot").value;
|
|
1946
|
+
const q = String($("kpQuery").value || "").trim().toLowerCase();
|
|
1947
|
+
const out = await api(`/api/kp/list?root=${encodeURIComponent(root)}&limit=2000`);
|
|
1948
|
+
let items = out.items || [];
|
|
1949
|
+
if (q) items = items.filter((p) => String(p.rel || "").toLowerCase().includes(q));
|
|
1950
|
+
state.kp = items;
|
|
1951
|
+
$("kpPill").textContent = String(items.length);
|
|
1952
|
+
$("kpListPill").textContent = String(items.length);
|
|
1953
|
+
renderKpNdcTree(items);
|
|
1954
|
+
}
|
|
1955
|
+
async function refresh() {
|
|
1956
|
+
if (state.ui.quitting) {
|
|
1957
|
+
stopSpinner();
|
|
1958
|
+
$("queuePill").textContent = "bye";
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
const meta = await api("/api/meta");
|
|
1963
|
+
$("meta").textContent = `cwd=${meta.cwd} server=${meta.url}`;
|
|
1964
|
+
} catch {
|
|
1965
|
+
}
|
|
1966
|
+
await refreshAvatar();
|
|
1967
|
+
try {
|
|
1968
|
+
const runs = await api("/api/runs?limit=25&includeFiles=1");
|
|
1969
|
+
const items = runs.items || [];
|
|
1970
|
+
const filesByRunId = runs.filesByRunId || {};
|
|
1971
|
+
const sig = items.map((r) => {
|
|
1972
|
+
const runId = String(r?.runId || "");
|
|
1973
|
+
const updatedAt = String(r?.updatedAt || "");
|
|
1974
|
+
const status = String(r?.status || "");
|
|
1975
|
+
const outLen = String(r?.lastOutput?.text || "").length;
|
|
1976
|
+
const fLen = Array.isArray(filesByRunId[runId]) ? filesByRunId[runId].length : 0;
|
|
1977
|
+
return `${runId}|${updatedAt}|${status}|${outLen}|${fLen}`;
|
|
1978
|
+
}).join(";");
|
|
1979
|
+
state.runs = items;
|
|
1980
|
+
state.runFilesById = filesByRunId;
|
|
1981
|
+
if (sig !== state.ui.lastRunsSig) {
|
|
1982
|
+
state.ui.lastRunsSig = sig;
|
|
1983
|
+
await renderRuns(state.runs);
|
|
1984
|
+
await ensureDefaultRunDetail();
|
|
1985
|
+
}
|
|
1986
|
+
} catch {
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
const logs = await api("/api/log/events?limit=120");
|
|
1990
|
+
renderLogs(logs.items || []);
|
|
1991
|
+
} catch {
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
const runsHist = await api("/api/runs?limit=200");
|
|
1995
|
+
const histItems = runsHist.items || [];
|
|
1996
|
+
const next = histItems.map((r) => String(r?.input || "").trim()).filter(Boolean);
|
|
1997
|
+
const prev = state.history.slice();
|
|
1998
|
+
const oldIdx = state.histIdx;
|
|
1999
|
+
const cur = oldIdx >= 0 && oldIdx < prev.length ? String(prev[oldIdx] || "") : "";
|
|
2000
|
+
state.history = next;
|
|
2001
|
+
if (oldIdx !== -1) {
|
|
2002
|
+
const maxK = 8;
|
|
2003
|
+
const prevK = prev.slice(0, Math.min(maxK, prev.length));
|
|
2004
|
+
let bestOffset = -1;
|
|
2005
|
+
let bestK = 0;
|
|
2006
|
+
for (let off = 0; off <= Math.min(80, Math.max(0, next.length - 1)); off++) {
|
|
2007
|
+
let k = 0;
|
|
2008
|
+
for (; k < prevK.length; k++) {
|
|
2009
|
+
if (String(next[off + k] || "") !== String(prevK[k] || "")) break;
|
|
2010
|
+
}
|
|
2011
|
+
if (k > bestK) {
|
|
2012
|
+
bestK = k;
|
|
2013
|
+
bestOffset = off;
|
|
2014
|
+
if (bestK === prevK.length) break;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (bestOffset >= 0 && bestK >= 1) {
|
|
2018
|
+
const shifted = oldIdx + bestOffset;
|
|
2019
|
+
state.histIdx = shifted >= 0 && shifted < next.length ? shifted : -1;
|
|
2020
|
+
} else if (cur) {
|
|
2021
|
+
const hits = [];
|
|
2022
|
+
for (let i = 0; i < next.length; i++) if (String(next[i] || "") === cur) hits.push(i);
|
|
2023
|
+
if (hits.length) {
|
|
2024
|
+
let best = hits[0];
|
|
2025
|
+
let bestD = Math.abs(best - oldIdx);
|
|
2026
|
+
for (const h of hits) {
|
|
2027
|
+
const d = Math.abs(h - oldIdx);
|
|
2028
|
+
if (d < bestD) {
|
|
2029
|
+
best = h;
|
|
2030
|
+
bestD = d;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
state.histIdx = best;
|
|
2034
|
+
} else {
|
|
2035
|
+
state.histIdx = -1;
|
|
2036
|
+
}
|
|
2037
|
+
} else {
|
|
2038
|
+
state.histIdx = -1;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (state.histIdx >= state.history.length) state.histIdx = -1;
|
|
2042
|
+
renderHistory(state.history);
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
const jobs = await api("/api/jobs");
|
|
2047
|
+
state.jobs = jobs.items || [];
|
|
2048
|
+
renderJobs(state.jobs);
|
|
2049
|
+
} catch {
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
async function loadUniverse() {
|
|
2053
|
+
const kind = $("uniKind").value;
|
|
2054
|
+
const name = $("uniName").value.trim();
|
|
2055
|
+
if (!name) return;
|
|
2056
|
+
try {
|
|
2057
|
+
const spec = await api(`/api/universe/spec?kind=${encodeURIComponent(kind)}&name=${encodeURIComponent(name)}`);
|
|
2058
|
+
state.uni.spec = spec.spec || null;
|
|
2059
|
+
} catch {
|
|
2060
|
+
state.uni.spec = null;
|
|
2061
|
+
}
|
|
2062
|
+
try {
|
|
2063
|
+
const data = await api(`/api/universe/contexts?kind=${encodeURIComponent(kind)}&name=${encodeURIComponent(name)}&tail=600`);
|
|
2064
|
+
state.uni.contexts = data.ok ? data : { task: [], common: [] };
|
|
2065
|
+
} catch {
|
|
2066
|
+
state.uni.contexts = { task: [], common: [] };
|
|
2067
|
+
}
|
|
2068
|
+
state.uni.seek = 0;
|
|
2069
|
+
state.uni.selectedNodeId = String(state.uni.spec && state.uni.spec.root ? state.uni.spec.root.id : "");
|
|
2070
|
+
updateUniverseReplay();
|
|
2071
|
+
renderNodeDetail(state.uni.selectedNodeId || "");
|
|
2072
|
+
}
|
|
2073
|
+
function attach() {
|
|
2074
|
+
$("tabs").addEventListener("click", (e) => {
|
|
2075
|
+
const t = e.target?.closest ? e.target.closest(".tab") : null;
|
|
2076
|
+
if (!t) return;
|
|
2077
|
+
setTab(String(t.dataset.tab || "runs"));
|
|
2078
|
+
});
|
|
2079
|
+
const themeSel = document.getElementById("themeSel");
|
|
2080
|
+
if (themeSel) {
|
|
2081
|
+
themeSel.addEventListener("change", () => applyTheme(themeSel.value, { persist: true }));
|
|
2082
|
+
}
|
|
2083
|
+
const flowDirSel = document.getElementById("flowDirSel");
|
|
2084
|
+
if (flowDirSel) {
|
|
2085
|
+
flowDirSel.addEventListener("change", () => {
|
|
2086
|
+
try {
|
|
2087
|
+
localStorage.setItem("maria.gui.flowDir", flowDirSel.value === "vertical" ? "vertical" : "horizontal");
|
|
2088
|
+
} catch {
|
|
2089
|
+
}
|
|
2090
|
+
api("/api/gui/state", {
|
|
2091
|
+
method: "POST",
|
|
2092
|
+
headers: { "content-type": "application/json" },
|
|
2093
|
+
body: JSON.stringify({ state: { flowDir: flowDirSel.value === "vertical" ? "vertical" : "horizontal" } })
|
|
2094
|
+
}).catch(() => {
|
|
2095
|
+
});
|
|
2096
|
+
updateUniverseReplay();
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
const flowParallelSel = document.getElementById("flowParallelSel");
|
|
2100
|
+
if (flowParallelSel) {
|
|
2101
|
+
flowParallelSel.addEventListener("change", () => {
|
|
2102
|
+
try {
|
|
2103
|
+
localStorage.setItem("maria.gui.flowParallel", flowParallelSel.value === "off" ? "off" : "on");
|
|
2104
|
+
} catch {
|
|
2105
|
+
}
|
|
2106
|
+
api("/api/gui/state", {
|
|
2107
|
+
method: "POST",
|
|
2108
|
+
headers: { "content-type": "application/json" },
|
|
2109
|
+
body: JSON.stringify({ state: { flowParallel: flowParallelSel.value === "off" ? "off" : "on" } })
|
|
2110
|
+
}).catch(() => {
|
|
2111
|
+
});
|
|
2112
|
+
updateUniverseReplay();
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
$("cmdRun").addEventListener("click", () => void runCmd($("cmdInput").value));
|
|
2116
|
+
const cmdInputEl = $("cmdInput");
|
|
2117
|
+
let cmdComposing = false;
|
|
2118
|
+
cmdInputEl.addEventListener("compositionstart", () => {
|
|
2119
|
+
cmdComposing = true;
|
|
2120
|
+
});
|
|
2121
|
+
cmdInputEl.addEventListener("compositionend", () => {
|
|
2122
|
+
cmdComposing = false;
|
|
2123
|
+
});
|
|
2124
|
+
cmdInputEl.addEventListener("keydown", (e) => {
|
|
2125
|
+
const ke = e;
|
|
2126
|
+
if (ke.key === "Enter") {
|
|
2127
|
+
if (ke.isComposing === true || cmdComposing || ke.keyCode === 229) return;
|
|
2128
|
+
if (ke.shiftKey) return;
|
|
2129
|
+
ke.preventDefault();
|
|
2130
|
+
void runCmd(cmdInputEl.value);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (ke.key === "ArrowUp") {
|
|
2134
|
+
if (!state.history.length) return;
|
|
2135
|
+
if (cmdInputEl.selectionStart !== cmdInputEl.selectionEnd) return;
|
|
2136
|
+
if ((cmdInputEl.selectionStart ?? 0) > 0) return;
|
|
2137
|
+
ke.preventDefault();
|
|
2138
|
+
state.histIdx = Math.min(state.history.length - 1, state.histIdx + 1);
|
|
2139
|
+
$("cmdInput").value = state.history[state.histIdx] || "";
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (ke.key === "ArrowDown") {
|
|
2143
|
+
if (!state.history.length) return;
|
|
2144
|
+
if (cmdInputEl.selectionStart !== cmdInputEl.selectionEnd) return;
|
|
2145
|
+
if ((cmdInputEl.selectionStart ?? 0) < cmdInputEl.value.length) return;
|
|
2146
|
+
ke.preventDefault();
|
|
2147
|
+
state.histIdx = Math.max(-1, state.histIdx - 1);
|
|
2148
|
+
$("cmdInput").value = state.histIdx === -1 ? "" : state.history[state.histIdx] || "";
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
$("avatarBox").addEventListener("click", () => $("avatarFile").click());
|
|
2152
|
+
$("avatarFile").addEventListener("change", async () => {
|
|
2153
|
+
const f = $("avatarFile").files?.[0];
|
|
2154
|
+
if (!f) return;
|
|
2155
|
+
const buf = await f.arrayBuffer();
|
|
2156
|
+
const bytesBase64 = arrayBufferToBase64(buf);
|
|
2157
|
+
await api("/api/avatar", {
|
|
2158
|
+
method: "POST",
|
|
2159
|
+
headers: { "content-type": "application/json" },
|
|
2160
|
+
body: JSON.stringify({ filename: f.name, mime: f.type || "image/png", bytesBase64 })
|
|
2161
|
+
});
|
|
2162
|
+
await refreshAvatar();
|
|
2163
|
+
$("avatarFile").value = "";
|
|
2164
|
+
});
|
|
2165
|
+
$("uniLoad").addEventListener("click", () => void loadUniverse());
|
|
2166
|
+
$("uniSeek").addEventListener("input", () => {
|
|
2167
|
+
state.uni.seek = Number($("uniSeek").value || "0");
|
|
2168
|
+
updateUniverseReplay();
|
|
2169
|
+
});
|
|
2170
|
+
$("uniPlay").addEventListener("click", () => {
|
|
2171
|
+
if (state.uni.playing) return;
|
|
2172
|
+
state.uni.playing = true;
|
|
2173
|
+
const tick = () => {
|
|
2174
|
+
if (!state.uni.playing) return;
|
|
2175
|
+
const sp = Number($("uniSpeed").value || "1");
|
|
2176
|
+
state.uni.seek = Math.min(state.uni.edges.length, state.uni.seek + sp);
|
|
2177
|
+
$("uniSeek").value = String(state.uni.seek);
|
|
2178
|
+
updateUniverseReplay();
|
|
2179
|
+
if (state.uni.seek >= state.uni.edges.length) {
|
|
2180
|
+
state.uni.playing = false;
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
state.uni.timer = window.setTimeout(tick, 120);
|
|
2184
|
+
};
|
|
2185
|
+
tick();
|
|
2186
|
+
});
|
|
2187
|
+
$("uniPause").addEventListener("click", () => {
|
|
2188
|
+
state.uni.playing = false;
|
|
2189
|
+
if (state.uni.timer != null) window.clearTimeout(state.uni.timer);
|
|
2190
|
+
});
|
|
2191
|
+
attachUniverseInteractions();
|
|
2192
|
+
$("mvQuery").addEventListener("input", () => renderMultiverseTree());
|
|
2193
|
+
$("mvRefresh").addEventListener("click", () => void runCmd("/universe multiverse").then(() => void loadMultiverse()));
|
|
2194
|
+
$("kpLoad").addEventListener("click", () => void loadKp());
|
|
2195
|
+
const kpMode = document.getElementById("kpMode");
|
|
2196
|
+
if (kpMode) kpMode.addEventListener("change", () => void loadKp());
|
|
2197
|
+
}
|
|
2198
|
+
async function boot() {
|
|
2199
|
+
await initThemeFromServer();
|
|
2200
|
+
await initFlowViewOptionsFromServer();
|
|
2201
|
+
attach();
|
|
2202
|
+
attachDragAndDropPath();
|
|
2203
|
+
await refresh();
|
|
2204
|
+
await loadMultiverse().catch(() => {
|
|
2205
|
+
});
|
|
2206
|
+
await loadKp().catch(() => {
|
|
2207
|
+
});
|
|
2208
|
+
state.ui.refreshTimer = window.setInterval(() => void refresh(), 2e3);
|
|
2209
|
+
}
|
|
2210
|
+
void boot();
|