@floless/app 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/floless-server.cjs +77 -2
- package/dist/web/app.css +27 -0
- package/dist/web/app.js +4 -1
- package/dist/web/aware.js +112 -0
- package/dist/web/steel-editor.html +120 -3
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -51016,6 +51016,8 @@ function readApp(id) {
|
|
|
51016
51016
|
kind: locked && typeof locked.kind === "string" ? locked.kind : "agent",
|
|
51017
51017
|
mode: lockMode === "write" || lockMode === "read" ? lockMode : "unknown",
|
|
51018
51018
|
runtimeModel: !!locked && locked["runtime-model"] === true,
|
|
51019
|
+
// `frozen:` lives on the SOURCE node (the user froze it); present (non-null) = frozen.
|
|
51020
|
+
frozen: n.frozen != null,
|
|
51019
51021
|
inputs: asRecord(locked?.inputs),
|
|
51020
51022
|
config: asRecord(n.config),
|
|
51021
51023
|
notes: notes2,
|
|
@@ -51627,6 +51629,10 @@ function assertId(id) {
|
|
|
51627
51629
|
if (!APP_ID.test(id)) throw new AwareError(`invalid app id: ${JSON.stringify(id)}`);
|
|
51628
51630
|
return id;
|
|
51629
51631
|
}
|
|
51632
|
+
function assertNodeId(id) {
|
|
51633
|
+
if (!APP_ID.test(id)) throw new AwareError(`invalid node id: ${JSON.stringify(id)}`);
|
|
51634
|
+
return id;
|
|
51635
|
+
}
|
|
51630
51636
|
var SOURCE_EXT = /\.(flo|app|flow|aware)$/i;
|
|
51631
51637
|
function assertSourcePath(p) {
|
|
51632
51638
|
if (!SOURCE_EXT.test(p)) throw new AwareError(`source must be a .flo/.app file: ${JSON.stringify(p)}`);
|
|
@@ -52008,6 +52014,39 @@ var aware = {
|
|
|
52008
52014
|
}
|
|
52009
52015
|
return { compiled: /lock refreshed/i.test(stdout), output: stdout.trim() };
|
|
52010
52016
|
},
|
|
52017
|
+
/**
|
|
52018
|
+
* Freeze a node (`aware app freeze <app> <node>`, CLI ≥ 0.77, aware-aeco#261). Pins the
|
|
52019
|
+
* node's LAST run output into the source as a `frozen:` block so Run skips the agent
|
|
52020
|
+
* (replays the pinned value, never re-runs it) and recompiles the lock. Fails when the
|
|
52021
|
+
* node has never produced output (nothing to pin) — the route relays that as "run it
|
|
52022
|
+
* once first". Throws AwareUnsupportedError on a pre-0.77 CLI so the route can say "update
|
|
52023
|
+
* AWARE" instead of treating an old CLI as a hard fault (mirrors rename/duplicate).
|
|
52024
|
+
*/
|
|
52025
|
+
async freeze(appId, nodeId) {
|
|
52026
|
+
const { code, stdout, stderr } = await runRaw(["app", "freeze", assertId(appId), assertNodeId(nodeId)]);
|
|
52027
|
+
if (code !== 0) {
|
|
52028
|
+
if (isInvokeUnsupported({ stdout, stderr })) {
|
|
52029
|
+
throw new AwareUnsupportedError("aware CLI does not support `app freeze` (needs \u2265 0.77) \u2014 update AWARE", { stdout, stderr });
|
|
52030
|
+
}
|
|
52031
|
+
throw new AwareError(`aware app freeze ${nodeId} failed (exit ${code})`, { stdout, stderr, code });
|
|
52032
|
+
}
|
|
52033
|
+
return { output: stdout.trim() };
|
|
52034
|
+
},
|
|
52035
|
+
/**
|
|
52036
|
+
* Unfreeze a node (`aware app unfreeze <app> <node>`, CLI ≥ 0.77). Removes the node's
|
|
52037
|
+
* `frozen:` block so it runs normally again and recompiles. Same unsupported-degradation
|
|
52038
|
+
* contract as freeze.
|
|
52039
|
+
*/
|
|
52040
|
+
async unfreeze(appId, nodeId) {
|
|
52041
|
+
const { code, stdout, stderr } = await runRaw(["app", "unfreeze", assertId(appId), assertNodeId(nodeId)]);
|
|
52042
|
+
if (code !== 0) {
|
|
52043
|
+
if (isInvokeUnsupported({ stdout, stderr })) {
|
|
52044
|
+
throw new AwareUnsupportedError("aware CLI does not support `app unfreeze` (needs \u2265 0.77) \u2014 update AWARE", { stdout, stderr });
|
|
52045
|
+
}
|
|
52046
|
+
throw new AwareError(`aware app unfreeze ${nodeId} failed (exit ${code})`, { stdout, stderr, code });
|
|
52047
|
+
}
|
|
52048
|
+
return { output: stdout.trim() };
|
|
52049
|
+
},
|
|
52011
52050
|
/**
|
|
52012
52051
|
* Install a first-party agent by registry id (`aware agent install <id>`). The
|
|
52013
52052
|
* registry install pulls a large tarball (~200s observed) — far over runRaw's 60s
|
|
@@ -52796,7 +52835,7 @@ function appVersion() {
|
|
|
52796
52835
|
return resolveVersion({
|
|
52797
52836
|
isSea: isSea2(),
|
|
52798
52837
|
sqVersionXml: readSqVersionXml(),
|
|
52799
|
-
define: true ? "0.
|
|
52838
|
+
define: true ? "0.31.0" : void 0,
|
|
52800
52839
|
pkgVersion: readPkgVersion()
|
|
52801
52840
|
});
|
|
52802
52841
|
}
|
|
@@ -52806,7 +52845,7 @@ function resolveChannel(s) {
|
|
|
52806
52845
|
return "dev";
|
|
52807
52846
|
}
|
|
52808
52847
|
function appChannel() {
|
|
52809
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52848
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.31.0" : void 0 });
|
|
52810
52849
|
}
|
|
52811
52850
|
|
|
52812
52851
|
// oauth-presets.ts
|
|
@@ -58225,6 +58264,42 @@ async function startServer() {
|
|
|
58225
58264
|
broadcast({ type: "apps-changed", id: req.params.id });
|
|
58226
58265
|
return { ok: true };
|
|
58227
58266
|
});
|
|
58267
|
+
app.post("/api/app/:id/freeze", async (req, reply) => {
|
|
58268
|
+
const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId.trim() : "";
|
|
58269
|
+
if (!nodeId) return reply.status(400).send({ ok: false, error: "nodeId required" });
|
|
58270
|
+
try {
|
|
58271
|
+
await aware.freeze(req.params.id, nodeId);
|
|
58272
|
+
} catch (e) {
|
|
58273
|
+
if (e instanceof AwareUnsupportedError) {
|
|
58274
|
+
return reply.status(422).send({ ok: false, error: "Freezing needs AWARE 0.77 or newer \u2014 update AWARE from the footer." });
|
|
58275
|
+
}
|
|
58276
|
+
if (e instanceof AwareError) {
|
|
58277
|
+
app.log.warn({ id: req.params.id, nodeId, detail: e.detail }, "freeze failed");
|
|
58278
|
+
return reply.status(422).send({ ok: false, error: "Couldn't freeze this node \u2014 it may have no saved output yet (run it once first).", reason: awareStderr(e) });
|
|
58279
|
+
}
|
|
58280
|
+
throw e;
|
|
58281
|
+
}
|
|
58282
|
+
broadcast({ type: "apps-changed", id: req.params.id });
|
|
58283
|
+
return { ok: true };
|
|
58284
|
+
});
|
|
58285
|
+
app.post("/api/app/:id/unfreeze", async (req, reply) => {
|
|
58286
|
+
const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId.trim() : "";
|
|
58287
|
+
if (!nodeId) return reply.status(400).send({ ok: false, error: "nodeId required" });
|
|
58288
|
+
try {
|
|
58289
|
+
await aware.unfreeze(req.params.id, nodeId);
|
|
58290
|
+
} catch (e) {
|
|
58291
|
+
if (e instanceof AwareUnsupportedError) {
|
|
58292
|
+
return reply.status(422).send({ ok: false, error: "Freezing needs AWARE 0.77 or newer \u2014 update AWARE from the footer." });
|
|
58293
|
+
}
|
|
58294
|
+
if (e instanceof AwareError) {
|
|
58295
|
+
app.log.warn({ id: req.params.id, nodeId, detail: e.detail }, "unfreeze failed");
|
|
58296
|
+
return reply.status(422).send({ ok: false, error: "Could not unfreeze this node.", reason: awareStderr(e) });
|
|
58297
|
+
}
|
|
58298
|
+
throw e;
|
|
58299
|
+
}
|
|
58300
|
+
broadcast({ type: "apps-changed", id: req.params.id });
|
|
58301
|
+
return { ok: true };
|
|
58302
|
+
});
|
|
58228
58303
|
app.post("/api/compile", async (req, reply) => {
|
|
58229
58304
|
const { id, sourcePath } = req.body ?? {};
|
|
58230
58305
|
const path = sourcePath ?? (id ? readApp(id).source.path : void 0);
|
package/dist/web/app.css
CHANGED
|
@@ -608,6 +608,20 @@
|
|
|
608
608
|
border: 1px solid var(--accent-dim); border-radius: 999px;
|
|
609
609
|
cursor: help;
|
|
610
610
|
}
|
|
611
|
+
/* Frozen node (aware app freeze): its last output is pinned and Run skips this node.
|
|
612
|
+
A quiet, INERT read — badge + a one-step-cooler surface — NOT a disabled state (the
|
|
613
|
+
card stays fully interactive; it's just static). Mirrors the rt-model-badge recipe but
|
|
614
|
+
with neutral-cool tokens so it reads distinct from the accent-blue runtime-model badge.
|
|
615
|
+
(No left ::after ribbon: that pseudo-element is already the node mode stripe.) */
|
|
616
|
+
.agent-card.frozen { background: var(--surface-2); }
|
|
617
|
+
.agent-card .frozen-badge {
|
|
618
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
619
|
+
margin: 0 0 8px; padding: 2px 8px;
|
|
620
|
+
font: 600 10px/1.4 var(--ui); letter-spacing: 0.01em;
|
|
621
|
+
color: var(--text-muted); background: var(--surface-3);
|
|
622
|
+
border: 1px solid var(--border-strong); border-radius: 999px;
|
|
623
|
+
cursor: help;
|
|
624
|
+
}
|
|
611
625
|
.agent-card .footer-row {
|
|
612
626
|
display: flex;
|
|
613
627
|
justify-content: space-between;
|
|
@@ -1855,6 +1869,19 @@
|
|
|
1855
1869
|
background: var(--border);
|
|
1856
1870
|
margin: 5px 4px;
|
|
1857
1871
|
}
|
|
1872
|
+
/* Cursor-anchored node context menu (right-click a card). Reuses .menu/.menu-item; only
|
|
1873
|
+
the fixed 280px width is wrong for it — the pointer anchor (top/left) is set inline. */
|
|
1874
|
+
.menu.context-menu { width: auto; min-width: 168px; }
|
|
1875
|
+
/* Non-interactive node-name header line atop the context menu. */
|
|
1876
|
+
.menu-head {
|
|
1877
|
+
padding: 6px 9px 4px;
|
|
1878
|
+
font-size: 11px;
|
|
1879
|
+
color: var(--text-dim);
|
|
1880
|
+
max-width: 220px;
|
|
1881
|
+
overflow: hidden;
|
|
1882
|
+
text-overflow: ellipsis;
|
|
1883
|
+
white-space: nowrap;
|
|
1884
|
+
}
|
|
1858
1885
|
.menu-theme {
|
|
1859
1886
|
display: flex;
|
|
1860
1887
|
align-items: center;
|
package/dist/web/app.js
CHANGED
|
@@ -339,7 +339,9 @@ function cardEl(id, ports) {
|
|
|
339
339
|
const a = AGENTS[id];
|
|
340
340
|
ports = ports || { in: 0, out: 0 };
|
|
341
341
|
const div = document.createElement('div');
|
|
342
|
-
|
|
342
|
+
// `frozen` = the node's output is pinned and Run skips it (aware app freeze). A quiet,
|
|
343
|
+
// inert card treatment + badge so it reads as "static / won't re-run" at a glance.
|
|
344
|
+
div.className = 'agent-card' + (a._frozen ? ' frozen' : '');
|
|
343
345
|
div.dataset.agentId = id;
|
|
344
346
|
div.draggable = true;
|
|
345
347
|
const faved = state.favorites.some(f => f.id === id);
|
|
@@ -355,6 +357,7 @@ function cardEl(id, ports) {
|
|
|
355
357
|
<div class="title">${a.title}</div>
|
|
356
358
|
<div class="subtitle">${a.subtitle}</div>
|
|
357
359
|
${a._runtimeModel ? '<div class="rt-model-badge" title="This node calls a model at run time (vision.extract). Output is content-cached and sits behind an approve gate — review the extraction before any write.">⚡ calls a model at run time</div>' : ''}
|
|
360
|
+
${a._frozen ? '<div class="frozen-badge" title="Frozen — its last result is pinned and Run skips this node. Right-click to unfreeze.">❄ Frozen</div>' : ''}
|
|
358
361
|
<div class="blurb">${a.blurb}</div>
|
|
359
362
|
<div class="footer-row">
|
|
360
363
|
<span class="ports"><span class="ports-num">${ports.in}</span> in <span class="ports-arrow">·</span> <span class="ports-num">${ports.out}</span> out</span>
|
package/dist/web/aware.js
CHANGED
|
@@ -294,6 +294,7 @@
|
|
|
294
294
|
return {
|
|
295
295
|
_mode: n.mode,
|
|
296
296
|
_runtimeModel: !!n.runtimeModel, // B2: lock stamped runtime-model → card badge
|
|
297
|
+
_frozen: !!n.frozen, // source node carries a `frozen:` block → card badge + Unfreeze menu
|
|
297
298
|
icon: iconFor(n.agent),
|
|
298
299
|
kind: n.kind === 'agent' && n.agent ? `${agentLabel} agent` : escapeHtml(n.kind),
|
|
299
300
|
version: pin ? `v${escapeHtml(String(pin))}` : '—',
|
|
@@ -1392,6 +1393,106 @@
|
|
|
1392
1393
|
}
|
|
1393
1394
|
}
|
|
1394
1395
|
|
|
1396
|
+
// ── Right-click Freeze / Unfreeze a node (aware-aeco#261) ─────────────────────
|
|
1397
|
+
// A general per-node toggle: right-click a canvas node → Freeze output (Run then
|
|
1398
|
+
// REPLAYS the node's last result instead of re-running its agent — so e.g. a
|
|
1399
|
+
// drawing-reader node won't re-read & wipe the user's edits, "whatever's there is
|
|
1400
|
+
// there") or Unfreeze (runs normally again). Needs AWARE ≥ 0.77; on an older runtime
|
|
1401
|
+
// the menu simply doesn't appear and the native menu passes through (the app keeps
|
|
1402
|
+
// AWARE at latest, so that's rare/transient — never a dead/disabled control).
|
|
1403
|
+
let _freezeMenuEl = null, _freezeMenuEsc = null, _freezeMenuOutside = null;
|
|
1404
|
+
|
|
1405
|
+
// numeric major.minor.patch compare; an empty/unparseable version sorts BELOW (-1),
|
|
1406
|
+
// so a missing awareVersion safely reads as "freeze unsupported". `Number` (not parseInt)
|
|
1407
|
+
// is strict — a prerelease segment like "0-rc1" → NaN → unsupported (defer to the server
|
|
1408
|
+
// backstop), never silently treated as the release.
|
|
1409
|
+
function cmpSemver(a, b) {
|
|
1410
|
+
const pa = String(a || '').split('.').map((x) => Number(x));
|
|
1411
|
+
const pb = String(b || '').split('.').map((x) => Number(x));
|
|
1412
|
+
if (pa.length < 3 || pa.some(Number.isNaN)) return -1;
|
|
1413
|
+
for (let i = 0; i < 3; i++) {
|
|
1414
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
1415
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
1416
|
+
}
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
function awareSupportsFreeze() { return cmpSemver(awareRuntimeVersion, '0.77.0') >= 0; }
|
|
1420
|
+
|
|
1421
|
+
function closeNodeMenu() {
|
|
1422
|
+
if (_freezeMenuEsc) { document.removeEventListener('keydown', _freezeMenuEsc, true); _freezeMenuEsc = null; }
|
|
1423
|
+
if (_freezeMenuOutside) { document.removeEventListener('mousedown', _freezeMenuOutside, true); _freezeMenuOutside = null; }
|
|
1424
|
+
if (_freezeMenuEl) { _freezeMenuEl.remove(); _freezeMenuEl = null; }
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Build + open the cursor-anchored menu over a node card. Reuses the hamburger
|
|
1428
|
+
// `.menu`/`.menu-item` vocabulary (+ a `.context-menu` width modifier) so it matches
|
|
1429
|
+
// the existing popover language; dismiss mirrors openNotesPopover (Escape + click-away).
|
|
1430
|
+
function openNodeMenu(card, x, y) {
|
|
1431
|
+
closeNodeMenu();
|
|
1432
|
+
const nodeId = card.dataset.agentId;
|
|
1433
|
+
if (!nodeId) return;
|
|
1434
|
+
const node = AGENTS[nodeId];
|
|
1435
|
+
const frozen = !!(node && node._frozen);
|
|
1436
|
+
// A 4-line snowflake glyph as a stroke SVG — matches `.menu-icon svg` (Lucide family).
|
|
1437
|
+
const ice = '<span class="menu-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="2" x2="12" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="4.6" y1="4.6" x2="19.4" y2="19.4"/><line x1="19.4" y1="4.6" x2="4.6" y2="19.4"/></svg></span>';
|
|
1438
|
+
const menu = document.createElement('div');
|
|
1439
|
+
menu.className = 'menu context-menu show';
|
|
1440
|
+
menu.setAttribute('role', 'menu');
|
|
1441
|
+
menu.setAttribute('aria-label', 'Node actions');
|
|
1442
|
+
menu.innerHTML =
|
|
1443
|
+
`<div class="menu-head" role="none">${escapeHtml(nodeId)}</div>`
|
|
1444
|
+
+ `<div class="menu-divider"></div>`
|
|
1445
|
+
+ `<button type="button" class="menu-item" role="menuitem">${ice}<span class="menu-label">${frozen ? 'Unfreeze' : 'Freeze output'}</span></button>`;
|
|
1446
|
+
document.body.appendChild(menu);
|
|
1447
|
+
// Anchor at the pointer, then clamp so a card near the right/bottom edge can't push
|
|
1448
|
+
// the menu off-screen (same intent as openNotesPopover's edge-clamp).
|
|
1449
|
+
const r = menu.getBoundingClientRect();
|
|
1450
|
+
menu.style.left = Math.max(8, Math.min(x, window.innerWidth - r.width - 8)) + 'px';
|
|
1451
|
+
menu.style.top = Math.max(8, Math.min(y, window.innerHeight - r.height - 8)) + 'px';
|
|
1452
|
+
const item = menu.querySelector('.menu-item');
|
|
1453
|
+
item.onclick = () => { closeNodeMenu(); toggleFreeze(nodeId, frozen); };
|
|
1454
|
+
try { item.focus(); } catch { /* container handles focus */ }
|
|
1455
|
+
_freezeMenuEl = menu;
|
|
1456
|
+
_freezeMenuEsc = (e) => { if (e.key === 'Escape') { e.preventDefault(); closeNodeMenu(); try { card.focus(); } catch { /* card not focusable */ } } };
|
|
1457
|
+
document.addEventListener('keydown', _freezeMenuEsc, true);
|
|
1458
|
+
_freezeMenuOutside = (e) => { if (_freezeMenuEl && !_freezeMenuEl.contains(e.target)) closeNodeMenu(); };
|
|
1459
|
+
document.addEventListener('mousedown', _freezeMenuOutside, true);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Toggle a node's frozen state via the server (which shells `aware app freeze/unfreeze`
|
|
1463
|
+
// + recompiles), then re-read the app so the badge + state repaint. api() throws on any
|
|
1464
|
+
// failure (non-2xx OR in-band ok:false) with the server's plain-English message — e.g.
|
|
1465
|
+
// "No output to freeze — run this node at least once first." — so one catch covers it.
|
|
1466
|
+
async function toggleFreeze(nodeId, currentlyFrozen) {
|
|
1467
|
+
if (!currentId) return;
|
|
1468
|
+
const verb = currentlyFrozen ? 'unfreeze' : 'freeze';
|
|
1469
|
+
try {
|
|
1470
|
+
await api(`/api/app/${encodeURIComponent(currentId)}/${verb}`, { method: 'POST', body: JSON.stringify({ nodeId }) });
|
|
1471
|
+
await loadApp(currentId); // re-read → frozen badge/state repaints
|
|
1472
|
+
showToast(currentlyFrozen
|
|
1473
|
+
? 'Node unfrozen — it will run normally.'
|
|
1474
|
+
: 'Output frozen — this node will replay its last result on the next run.',
|
|
1475
|
+
currentlyFrozen ? 'info' : 'ok');
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
// Surface the real aware reason the route attached as body.reason — otherwise the friendly
|
|
1478
|
+
// lead hides the actual cause. First non-empty line, capped, keeps the warn toast sane.
|
|
1479
|
+
const lead = (e && e.message) || `Could not ${verb} this node.`;
|
|
1480
|
+
const raw = e && e.body && typeof e.body.reason === 'string' ? e.body.reason.trim() : '';
|
|
1481
|
+
const reason = raw ? (raw.split('\n').find((l) => l.trim()) || '').trim().slice(0, 200) : '';
|
|
1482
|
+
showToast(reason ? `${lead} — ${reason}` : lead, 'warn');
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Right-click a node card → the Freeze/Unfreeze menu. preventDefault is scoped to cards
|
|
1487
|
+
// (right-click stays normal everywhere else), and only when AWARE supports it — on an
|
|
1488
|
+
// older runtime the native menu passes through, so there's no dead interaction.
|
|
1489
|
+
document.addEventListener('contextmenu', (e) => {
|
|
1490
|
+
const card = e.target.closest && e.target.closest('.agent-card');
|
|
1491
|
+
if (!card || !awareSupportsFreeze()) return;
|
|
1492
|
+
e.preventDefault();
|
|
1493
|
+
openNodeMenu(card, e.clientX, e.clientY);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1395
1496
|
// Approve & bake — freezes the edited contract into the .lock, arming Run.
|
|
1396
1497
|
// Flushes the editor's pending debounce save first so Approve always bakes the
|
|
1397
1498
|
// latest in-memory edits, not a stale snapshot. api() throws on non-2xx, so
|
|
@@ -3284,6 +3385,8 @@
|
|
|
3284
3385
|
}
|
|
3285
3386
|
let shownVersion = false;
|
|
3286
3387
|
let loadedAppVersion = null; // the build this tab is running against (first health wins)
|
|
3388
|
+
let awareRuntimeVersion = null; // installed AWARE version (from /api/health) — gates the freeze menu
|
|
3389
|
+
let freezeHintAppended = false; // append the right-click-to-freeze canvas hint once, when supported
|
|
3287
3390
|
function startHealthPoll() {
|
|
3288
3391
|
const tick = async () => {
|
|
3289
3392
|
let ok = false;
|
|
@@ -3326,6 +3429,15 @@
|
|
|
3326
3429
|
// After the build version is stamped, reveal the relaunch-surviving what's-new
|
|
3327
3430
|
// panel iff this is the build we just self-updated into (guarded to once).
|
|
3328
3431
|
maybeShowWhatsNew();
|
|
3432
|
+
// Track the installed AWARE runtime version so the right-click freeze menu can
|
|
3433
|
+
// gate itself (needs ≥ 0.77) and the canvas hint can advertise it once available.
|
|
3434
|
+
if (h && h.awareVersion) {
|
|
3435
|
+
awareRuntimeVersion = h.awareVersion;
|
|
3436
|
+
if (!freezeHintAppended && awareSupportsFreeze()) {
|
|
3437
|
+
const ch = document.getElementById('canvas-hint');
|
|
3438
|
+
if (ch) { ch.textContent += ' Right-click a node to freeze its output.'; freezeHintAppended = true; }
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3329
3441
|
const wv = document.getElementById('aware-version');
|
|
3330
3442
|
if (wv && h && h.awareVersion) {
|
|
3331
3443
|
const next = 'AWARE ' + h.awareVersion;
|
|
@@ -61,12 +61,30 @@
|
|
|
61
61
|
rect.dethot{cursor:pointer;fill:rgba(168,85,247,.16);stroke:#a855f7;stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke}
|
|
62
62
|
rect.dethot:hover{fill:rgba(168,85,247,.34)}
|
|
63
63
|
text.dettx{fill:#ddd6fe;font:bold 11px system-ui;text-anchor:middle;pointer-events:none}
|
|
64
|
-
#detailsModal,#framesModal,#rfiModal,#askAiModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
|
|
64
|
+
#detailsModal,#framesModal,#rfiModal,#confModal,#askAiModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
|
|
65
65
|
#askAiDrop:hover{border-color:var(--brand);color:var(--text)} #askAiDrop.has{border-style:solid;border-color:var(--brand)}
|
|
66
66
|
.aithumb{position:relative;display:inline-flex} .aithumb img{height:60px;border-radius:4px;border:1px solid var(--line);background:#fff;display:block}
|
|
67
67
|
.aithumb button{position:absolute;top:-5px;right:-5px;width:16px;height:16px;padding:0;border-radius:8px;font-size:10px;line-height:16px;text-align:center;background:#7f1d1d;border-color:#991b1b;color:#fecaca}
|
|
68
68
|
#saveStat.dirty{color:#fbbf24} #saveStat.ok{color:#86efac} #saveStat.err{color:#fca5a5}
|
|
69
69
|
#rfiStat{cursor:pointer;text-decoration:underline dotted} #rfiStat:hover b{color:#fff}
|
|
70
|
+
#confStat{cursor:pointer;text-decoration:underline dotted} #confStat:hover b{filter:brightness(1.25)}
|
|
71
|
+
.conf-cats{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;padding:14px;border-bottom:1px solid var(--line)}
|
|
72
|
+
.ccard{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:10px 12px}
|
|
73
|
+
.ccard.click{cursor:pointer} .ccard.click:hover{border-color:var(--brand)} .ccard.on{border-color:var(--brand)}
|
|
74
|
+
.ccard .cc-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin-bottom:4px}
|
|
75
|
+
.ccard .cc-score{font-size:22px;font-weight:700;line-height:1;margin-bottom:6px}
|
|
76
|
+
.ccard .cc-bar{height:4px;border-radius:2px;background:var(--line);margin-bottom:6px;overflow:hidden}
|
|
77
|
+
.ccard .cc-fill{height:100%;border-radius:2px}
|
|
78
|
+
.ccard .cc-counts{font-size:11px;color:var(--mut);display:flex;gap:8px;flex-wrap:wrap}
|
|
79
|
+
.conf-filter{display:flex;gap:12px;flex-wrap:wrap;padding:12px 14px;border-bottom:1px solid var(--line);align-items:center}
|
|
80
|
+
.conf-filter .glab{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut)}
|
|
81
|
+
.conf-filter .grp{display:inline-flex;border:1px solid var(--line);border-radius:6px;overflow:hidden}
|
|
82
|
+
.conf-filter .grp button{border:0;border-radius:0;background:transparent;color:var(--mut);font-size:11px;padding:4px 9px;cursor:pointer}
|
|
83
|
+
.conf-filter .grp button.on{background:var(--brand);color:#fff}
|
|
84
|
+
.conf-body{padding:0 14px 14px;overflow:auto;flex:1;min-height:0}
|
|
85
|
+
.chip{display:inline-block;padding:1px 6px;border-radius:4px;font-size:10px;border:1px solid var(--line);color:var(--mut);margin:0 3px 3px 0}
|
|
86
|
+
.chip.assumed,.chip.weak{border-color:#713f12;color:#fbbf24} .chip.fail{border-color:#7f1d1d;color:#fca5a5}
|
|
87
|
+
tr.confrow{cursor:pointer} .conf-expand{color:var(--mut);font-size:12px;line-height:1.6;background:#0b1220}
|
|
70
88
|
.ftab td .combo{padding:4px 6px} .ftab .rea{color:var(--mut);font-size:12px} .rfichip{display:inline-block;min-width:18px;height:18px;line-height:18px;text-align:center;background:#7f1d1d;color:#fecaca;border-radius:9px;font-size:11px;padding:0 5px}
|
|
71
89
|
.mbackdrop{position:absolute;inset:0;background:rgba(2,8,23,.62)}
|
|
72
90
|
.mpanel{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:10px;width:min(880px,92vw);max-height:86vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.6)}
|
|
@@ -115,6 +133,7 @@
|
|
|
115
133
|
<span class=stat>Members <b id=mc>0</b></span><span class=stat>Weight <b id=wt>0</b> tons · <b id=wtlb>0</b> lb</span>
|
|
116
134
|
<span class=stat id=rfiStat title="Click to list unresolved members (RFI)">RFI <b id=rc>0</b></span>
|
|
117
135
|
<span class=stat id=dupStat title="Overlapping/duplicate members (same geometry)">Dup <b id=dpc>0</b></span>
|
|
136
|
+
<span class=stat id=confStat title="Confidence report — score each AI-read element by evidence" style="display:none">Confidence <b id=confPct>—</b></span>
|
|
118
137
|
<span class=stat id=saveStat title="Edits auto-save in this browser (localStorage)">Saved</span><span style="flex:1"></span>
|
|
119
138
|
<button id=undoB title="Undo (Ctrl+Z)">↶</button>
|
|
120
139
|
<button id=redoB title="Redo (Ctrl+Y / Ctrl+Shift+Z)">↷</button>
|
|
@@ -160,6 +179,11 @@
|
|
|
160
179
|
<div id=rfiModal><div class=mbackdrop id=rfiBackdrop></div>
|
|
161
180
|
<div class=mpanel><div class=mhead><b>Unresolved members — RFI</b><button id=rfiClose>✕</button></div>
|
|
162
181
|
<div id=rfiGrid style="padding:14px;overflow:auto"></div></div></div>
|
|
182
|
+
<div id=confModal><div class=mbackdrop id=confBackdrop></div>
|
|
183
|
+
<div class=mpanel><div class=mhead><b>Confidence report</b><button id=confClose>✕</button></div>
|
|
184
|
+
<div id=confCats class=conf-cats></div>
|
|
185
|
+
<div id=confFilter class=conf-filter></div>
|
|
186
|
+
<div id=confBody class=conf-body></div></div></div>
|
|
163
187
|
<div id=askAiModal><div class=mbackdrop id=askAiBackdrop></div>
|
|
164
188
|
<div class=mpanel style="width:min(560px,92vw)">
|
|
165
189
|
<div class=mhead><b>Ask the AI</b><button id=askAiClose>✕</button></div>
|
|
@@ -429,7 +453,7 @@ function render(){
|
|
|
429
453
|
selM.forEach((m,idx)=>{const c=mid(m),k=Math.round(c[0]/8)+','+Math.round(c[1]/8);(grp[k]=grp[k]||[]).push({idx,c});});
|
|
430
454
|
for(const k in grp){const a=grp[k],n=a.length;a.forEach((it,j)=>{const x=it.c[0]+(j-(n-1)/2)*R*2,y=it.c[1];const d=`data-bx="${it.c[0]}" data-fi="${j}" data-gn="${n}"`;
|
|
431
455
|
s+=`<circle class=numbg ${d} cx="${x}" cy="${y}" r="${R}"/><text class=numtx ${d} x="${x}" y="${y}" style="font-size:${F}px">${it.idx+1}</text>`;});}}
|
|
432
|
-
svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup();
|
|
456
|
+
svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup(); updConf();
|
|
433
457
|
}
|
|
434
458
|
function updDup(){const n=redundantDups().length;
|
|
435
459
|
document.getElementById('dpc').textContent=n;document.getElementById('dupStat').classList.toggle('has',n>0);
|
|
@@ -500,6 +524,7 @@ function panel(){
|
|
|
500
524
|
<div class=row><label>Profile</label><div style="display:flex;gap:6px"><input id=pf class=combo data-src=profiles value="${esc(m.profile)}" style="flex:1" autocomplete=off><button id=pickProf class="ghost${(picking&&pickKind==='profile')?' on':''}" title="Pick profile by clicking a label in the drawing">⌖ pick</button></div>${(picking&&pickKind==='profile')?'<div class="hint" style="margin-top:4px;font-style:italic;color:var(--brand)">Click a profile label in the drawing…</div>':(picking&&pickKind==='detail')?'<div class="hint" style="margin-top:4px;font-style:italic;color:#a855f7">Click a detail callout in the drawing…</div>':''}</div>
|
|
501
525
|
<div class="seg2 f"><button id=rBeam class="${col?'':'on'}">Beam</button><button id=rCol class="${col?'on':''}">Column</button></div>
|
|
502
526
|
<div class="row hint">Length <b>${L} ft</b> · ${wpf==null?'<span class=pill style="background:#7f1d1d">RFI — size unresolved</span>':'Weight <b>'+(len(m.wp[0],m.wp[1])/FT*wpf).toFixed(0)+' lb</b> · '+wpf+' lb/ft'}</div>
|
|
527
|
+
<div class=row><button class=ghostw id=verifyBtn${m.verified?' style="border-color:#166534;color:#86efac"':''} title="Mark this member human-confirmed → 100% in the confidence report">${m.verified?'✓ Verified — human-confirmed':'Mark verified'}</button></div>
|
|
503
528
|
${mfSug.length?`<div class="row" style="border:1px solid #a855f7;border-radius:6px;padding:7px 8px;background:rgba(168,85,247,.07)"><div class=elab style="color:#c4b5fd;margin:0">Moment-frame girder · ${_lvl==='roof'?'roof':'2nd floor'} (from Frames)</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:5px">${mfSug.map(s=>`<button class="ghost mfsug${s===m.profile?' on':''}" data-s="${esc(s)}">${esc(s)}</button>`).join('')}</div></div>`:''}
|
|
504
529
|
${elev}
|
|
505
530
|
<div class=divrow><hr><span class=sect style="margin:0">Modify geometry</span><hr></div>
|
|
@@ -510,7 +535,7 @@ function panel(){
|
|
|
510
535
|
document.getElementById('pickProf').onclick=()=>{if(picking&&pickKind==='profile'){picking=false;}else{picking=true;pickKind='profile';pickEnd=null;}render();};
|
|
511
536
|
document.getElementById('rBeam').onclick=()=>{if(col)edit(()=>{m.role='beam';});};
|
|
512
537
|
document.getElementById('rCol').onclick=()=>{if(!col)edit(()=>{m.role='column';});};
|
|
513
|
-
document.querySelectorAll('.mfsug').forEach(b=>b.onclick=()=>edit(()=>{m.profile=b.dataset.s;m.rfi=(WT[m.profile]==null);if(!profs.includes(m.profile)){profs.push(m.profile);profs.sort();}}));
|
|
538
|
+
document.querySelectorAll('.mfsug').forEach(b=>b.onclick=()=>edit(()=>{m.profile=b.dataset.s;m.mf=true;m.rfi=(WT[m.profile]==null);if(!profs.includes(m.profile)){profs.push(m.profile);profs.sort();}})); // record the moment-frame provenance (drives the confidence "schedule-resolved" factor)
|
|
514
539
|
const _gel=document.getElementById('geoEL'),_gsp=document.getElementById('geoSplit');
|
|
515
540
|
if(_gel)_gel.onclick=()=>{geoMode=(geoMode==='el'?null:'el');setGeo();render();};
|
|
516
541
|
if(_gsp)_gsp.onclick=()=>{geoMode=(geoMode==='split'?null:'split');setGeo();render();};
|
|
@@ -534,6 +559,7 @@ function panel(){
|
|
|
534
559
|
document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});
|
|
535
560
|
}
|
|
536
561
|
document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();});
|
|
562
|
+
{const vb=document.getElementById('verifyBtn');if(vb)vb.onclick=()=>edit(()=>{m.verified=!m.verified;});}
|
|
537
563
|
}
|
|
538
564
|
function addFromSeg(sid){const sg=P.segments.find(s=>s.id===sid);if(!sg)return;const P=addProfile;const id='m'+Date.now();const pv=snapshot();
|
|
539
565
|
P.members.push(ensureMeta({id,profile:P,wp:[sg.a.slice(),sg.b.slice()],angle:sg.o,rfi:(WT[P]==null)}));
|
|
@@ -643,6 +669,7 @@ addEventListener('keydown',e=>{
|
|
|
643
669
|
if(e.key==='Escape'&&detailsOpen()){closeDetails();return;}
|
|
644
670
|
if(e.key==='Escape'&&framesOpen()){closeFrames();return;}
|
|
645
671
|
if(e.key==='Escape'&&rfiOpen()){closeRFI();return;}
|
|
672
|
+
if(e.key==='Escape'&&confOpen()){closeConf();return;}
|
|
646
673
|
if(e.key==='Home'){e.preventDefault();fitToWindow();return;}
|
|
647
674
|
if(e.key==='Escape'){if(geoMode){geoMode=null;setGeo();render();return;}if(mode==='add'){mode='sel';setMode();}picking=false;pickKind='profile';pickEnd=null;selIds.clear();render();return;}
|
|
648
675
|
if((e.key==='Delete'||e.key==='Backspace')&&selIds.size&&!inForm){e.preventDefault();edit(()=>{P.members=P.members.filter(m=>!selIds.has(m.id));selIds.clear();});return;}
|
|
@@ -780,6 +807,96 @@ function rfiOpen(){return document.getElementById('rfiModal').style.display==='f
|
|
|
780
807
|
document.getElementById('rfiStat').onclick=openRFI;
|
|
781
808
|
document.getElementById('rfiClose').onclick=closeRFI;
|
|
782
809
|
document.getElementById('rfiBackdrop').onclick=closeRFI;
|
|
810
|
+
// ===== Confidence report — evidence-based score (MIRROR of server/steel-confidence.ts; keep in sync) =====
|
|
811
|
+
// Deterministic: scores ONLY observable evidence already in the contract, never a model self-report.
|
|
812
|
+
const BANDW={verified:1,high:.9,med:.65,low:.35,rfi:0};
|
|
813
|
+
const BANDLAB={verified:'Verified',high:'High',med:'Medium',low:'Low',rfi:'RFI'};
|
|
814
|
+
const BANDC={verified:'#86efac',high:'#4ade80',med:'#fbbf24',low:'#fca5a5',rfi:'#fca5a5'};
|
|
815
|
+
const BANDPILL={verified:'background:#166534;color:#86efac',high:'background:#14532d;color:#4ade80',med:'background:#713f12;color:#fbbf24',low:'background:#7f1d1d;color:#fca5a5',rfi:'background:#7f1d1d;color:#fca5a5'};
|
|
816
|
+
function _wt(profile){if(!WT||!profile)return null;if(profile in WT)return WT[profile]==null?null:WT[profile];
|
|
817
|
+
const h=Object.entries(WT).find(([k])=>k.toUpperCase()===profile.toUpperCase());return h?(h[1]==null?null:h[1]):null;}
|
|
818
|
+
const _isMf=p=>!!p&&/(^|[^A-Z])MF($|[^A-Z])/i.test(p);
|
|
819
|
+
function _confDupIds(members){const g={};const key=m=>{if(!m.wp||m.wp.length<2)return null;const r=p=>Math.round(p[0]/3)+','+Math.round(p[1]/3);const a=r(m.wp[0]),b=r(m.wp[1]);return a<b?a+'|'+b:b+'|'+a;};
|
|
820
|
+
const rank=m=>{let s=0;if(m.profile&&!_isMf(m.profile))s++;if(m.profile&&m.profile.trim()!=='')s++;return s;};
|
|
821
|
+
for(const m of members){const k=key(m);if(!k)continue;(g[k]=g[k]||[]).push(m);}
|
|
822
|
+
const out=new Set();for(const k in g){const grp=g[k];if(grp.length<2)continue;grp.sort((a,b)=>rank(b)-rank(a));for(let i=1;i<grp.length;i++)out.add(grp[i].id);}return out;}
|
|
823
|
+
function _elevAssumed(m){if(m.role==='column')return !(m.col&&m.col.tosDef===false);const en=m.ends||[];if(!en.length)return true;return en.some(e=>e.tosDef!==false);}
|
|
824
|
+
function _scoreMember(m,dup){const plf=_wt(m.profile);const F=[];
|
|
825
|
+
if(plf==null){F.push({key:'profile',label:'Profile',state:'fail',detail:(!m.profile||!m.profile.trim())?'no profile assigned':_isMf(m.profile)?('unresolved mark "'+m.profile+'" — not an AISC size'):('"'+m.profile+'" not in the AISC weight table')});return {band:'rfi',factors:F};}
|
|
826
|
+
const sched=m.mf===true;F.push({key:'profile',label:'Profile',state:sched?'assumed':'ok',detail:sched?(m.profile+' — resolved from the frame schedule ('+plf+' plf)'):(m.profile+' ('+plf+' plf, AISC)')});
|
|
827
|
+
const asm=_elevAssumed(m);F.push({key:'elevation',label:'Elevation',state:asm?'assumed':'ok',detail:asm?'plan default (UNO) — no local callout':'from a drawing callout'});
|
|
828
|
+
const isd=dup.has(m.id);if(isd)F.push({key:'dup',label:'Duplicate',state:'fail',detail:'coincident with a kept member — deleting dedupes the BOM'});
|
|
829
|
+
if(m.verified===true)F.push({key:'verified',label:'Verified',state:'ok',detail:'human-confirmed'});
|
|
830
|
+
const band=m.verified===true?'verified':isd?'low':(sched||asm)?'med':'high';return {band,factors:F};}
|
|
831
|
+
function _mTons(m,ptPerFt){const plf=_wt(m.profile);if(plf==null)return 0;let ft;
|
|
832
|
+
if(m.role==='column'){if(!m.col||m.col.tos==null||m.col.bos==null)return 0;ft=Math.abs(m.col.tos-m.col.bos)/12;} // column length = height (TOS−BOS), inches→ft
|
|
833
|
+
else{if(!m.wp||m.wp.length<2)return 0;ft=Math.hypot(m.wp[0][0]-m.wp[1][0],m.wp[0][1]-m.wp[1][1])/(ptPerFt>0?ptPerFt:1);}
|
|
834
|
+
return ft*plf/2000;}
|
|
835
|
+
function _detSheet(t){const m=String(t||'').toUpperCase().match(/S-?\s?\d{2,3}/);return m?m[0].replace(/\s/g,''):null;}
|
|
836
|
+
const _cnt0=()=>({verified:0,high:0,med:0,low:0,rfi:0});
|
|
837
|
+
function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[];let segs=0;
|
|
838
|
+
const known=new Set();for(const p of plans)if(p.sheet)known.add(p.sheet.toUpperCase());for(const s of Object.keys(C.detail_bubbles||{}))known.add(s.toUpperCase());
|
|
839
|
+
plans.forEach((plan,pi)=>{segs+=(plan.segments||[]).length;const ms=plan.members||[];const dup=_confDupIds(ms);
|
|
840
|
+
for(const m of ms){const r=_scoreMember(m,dup);byMember.push({id:m.id,planIdx:pi,sheet:plan.sheet||'',role:m.role==='column'?'column':'beam',profile:m.profile||'',band:r.band,tons:r.band==='rfi'?0:_mTons(m,plan.pt_per_ft),factors:r.factors});}
|
|
841
|
+
for(const d of (plan.details||[])){const sh=_detSheet(d.text);const ok=sh!=null&&known.has(sh.toUpperCase());byDetail.push({text:d.text||'',planIdx:pi,sheet:plan.sheet||'',band:ok?'high':'low',reason:ok?('references '+sh+' (in the set)'):sh?('references '+sh+' — not found in the set'):'no sheet reference parsed'});}});
|
|
842
|
+
const roll=(arr,cw)=>{const c=_cnt0();let n=0,d=0,t=0;for(const m of arr){c[m.band]++;if(m.band==='rfi')continue;const w=cw?1:m.tons;n+=w*BANDW[m.band];d+=w;t+=m.tons;}return {score:d>0?Math.round(n/d*100):null,tons:t,counts:c};};
|
|
843
|
+
const dRoll=arr=>{const c=_cnt0();let n=0,d=0;for(const x of arr){c[x.band]++;n+=BANDW[x.band];d++;}return {score:d>0?Math.round(n/d*100):null,tons:0,counts:c};};
|
|
844
|
+
const beams=roll(byMember.filter(m=>m.role==='beam'));const columns=roll(byMember.filter(m=>m.role==='column'));const details=dRoll(byDetail);
|
|
845
|
+
const sc=byMember.filter(m=>m.band!=='rfi');const n=sc.reduce((s,m)=>s+m.tons*BANDW[m.band],0),d=sc.reduce((s,m)=>s+m.tons,0);
|
|
846
|
+
return {byMember,byDetail,byCategory:{beams,columns,details,connections:{score:null,note:'Not scored yet (reserved slot)',counts:_cnt0()}},coverage:{members:byMember.length,segments:segs,note:'Approximate in v1 — precise per-segment binding is Slice 2.'},overall:{score:d>0?Math.round(n/d*100):null,tons:d,rfiCount:byMember.filter(m=>m.band==='rfi').length}};}
|
|
847
|
+
function bandColorForPct(p){return p==null?'var(--mut)':p>=95?BANDC.verified:p>=80?BANDC.high:p>=50?BANDC.med:BANDC.low;}
|
|
848
|
+
function updConf(){const el=document.getElementById('confStat');if(!el)return;const s=scoreContractJS();
|
|
849
|
+
if(!s.byMember.length){el.style.display='none';return;}el.style.display='';
|
|
850
|
+
const b=document.getElementById('confPct');b.textContent=s.overall.score==null?'—':s.overall.score+'%';b.style.color=bandColorForPct(s.overall.score);
|
|
851
|
+
el.title='Confidence — '+s.overall.tons.toFixed(1)+' t scored · '+s.overall.rfiCount+' RFI. Click for the report.';}
|
|
852
|
+
// --- the report modal ---
|
|
853
|
+
let confBand='all',confCat='all',confExpand=null;
|
|
854
|
+
function openConf(){confExpand=null;renderConf();document.getElementById('confModal').style.display='flex';}
|
|
855
|
+
function closeConf(){document.getElementById('confModal').style.display='none';}
|
|
856
|
+
function confOpen(){return document.getElementById('confModal').style.display==='flex';}
|
|
857
|
+
function _bar(pct){return '<div class=cc-bar><div class=cc-fill style="width:'+(pct||0)+'%;background:'+bandColorForPct(pct)+';'+(pct==null?'opacity:0':'')+'"></div></div>';}
|
|
858
|
+
function _countsHtml(c){const out=['verified','high','med','low','rfi'].filter(k=>c[k]>0).map(k=>'<span style="color:'+BANDC[k]+'">'+BANDLAB[k][0]+' <b>'+c[k]+'</b></span>').join('');return out||'<span>—</span>';}
|
|
859
|
+
function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
|
|
860
|
+
const card=(key,label,cs,stub)=>'<div class="ccard'+(stub?' stub':' click')+(confCat===key?' on':'')+'" data-cat="'+key+'"'+(stub?' title="not scored yet"':'')+'>'+
|
|
861
|
+
'<div class=cc-label>'+label+'</div>'+
|
|
862
|
+
'<div class=cc-score style="color:'+(stub?'var(--mut)':bandColorForPct(cs.score))+'">'+(stub||cs.score==null?'—':cs.score+'%')+'</div>'+
|
|
863
|
+
_bar(stub?null:cs.score)+
|
|
864
|
+
'<div class=cc-counts>'+(stub?'<span>reserved slot</span>':_countsHtml(cs.counts))+'</div></div>';
|
|
865
|
+
document.getElementById('confCats').innerHTML=card('beams','Beams',cat.beams)+card('columns','Columns',cat.columns)+card('details','Details',cat.details)+card('connections','Connections',cat.connections,true);
|
|
866
|
+
const bands=['all','verified','high','med','low','rfi'],cats=['all','beams','columns','details','connections'];
|
|
867
|
+
document.getElementById('confFilter').innerHTML=
|
|
868
|
+
'<span class=glab>Band</span><span class=grp>'+bands.map(b=>'<button data-band="'+b+'" class="'+(confBand===b?'on':'')+'">'+(b==='all'?'All':BANDLAB[b])+'</button>').join('')+'</span>'+
|
|
869
|
+
'<span class=glab>Category</span><span class=grp>'+cats.map(c=>'<button data-fcat="'+c+'" class="'+(confCat===c?'on':'')+'">'+(c==='all'?'All':c[0].toUpperCase()+c.slice(1))+'</button>').join('')+'</span>'+
|
|
870
|
+
'<span class=glab style="margin-left:auto">Coverage: '+s.coverage.members+' members · '+s.coverage.segments+' segments (approx, v1)</span>';
|
|
871
|
+
document.getElementById('confBody').innerHTML=confBodyHtml(s);
|
|
872
|
+
wireConf();}
|
|
873
|
+
function confBodyHtml(s){
|
|
874
|
+
if(!s.byMember.length&&!s.byDetail.length)return '<div class=hint style="padding:14px 0">No members in the takeoff yet.</div>';
|
|
875
|
+
if(confCat==='connections')return '<div class=hint style="padding:14px 0">Connection scoring is not built yet (reserved slot).</div>';
|
|
876
|
+
let mem=s.byMember.slice();
|
|
877
|
+
if(confCat==='beams')mem=mem.filter(m=>m.role==='beam');else if(confCat==='columns')mem=mem.filter(m=>m.role==='column');else if(confCat==='details')mem=[];
|
|
878
|
+
if(confBand!=='all')mem=mem.filter(m=>m.band===confBand);
|
|
879
|
+
let det=(confCat==='all'||confCat==='details')?s.byDetail.slice():[];
|
|
880
|
+
if(confBand!=='all')det=det.filter(d=>d.band===confBand);
|
|
881
|
+
let h='<table class=ftab><thead><tr><th>Band</th><th>Element</th><th>Sheet</th><th>Tons</th><th>Evidence</th><th></th></tr></thead><tbody>';
|
|
882
|
+
mem.forEach(m=>{const chips=m.factors.map(f=>'<span class="chip '+(f.state==='ok'?'':f.state)+'">'+esc(f.label)+(f.state==='ok'?' ✓':f.state==='fail'?' ✗':' ●')+'</span>').join('');
|
|
883
|
+
h+='<tr class=confrow data-mid="'+esc(m.id)+'" data-pi="'+m.planIdx+'"><td><span class=pill style="'+BANDPILL[m.band]+'">'+BANDLAB[m.band]+'</span></td>'+
|
|
884
|
+
'<td>'+(esc(m.profile)||'<span style="color:var(--mut)">(no profile)</span>')+' <span style="color:var(--mut);font-size:11px">'+esc(m.role)+'</span></td>'+
|
|
885
|
+
'<td>'+esc(m.sheet)+'</td><td>'+(m.tons?m.tons.toFixed(2):'—')+'</td><td>'+chips+'</td>'+
|
|
886
|
+
'<td><button class=ghost data-loc="'+esc(m.id)+'" data-pi="'+m.planIdx+'">Locate</button></td></tr>';
|
|
887
|
+
if(confExpand===m.id)h+='<tr><td></td><td colspan=5 class=conf-expand>'+m.factors.map(f=>'<div>• <b>'+esc(f.label)+':</b> '+esc(f.detail)+'</div>').join('')+'</td></tr>';});
|
|
888
|
+
det.forEach(d=>{h+='<tr><td><span class=pill style="'+BANDPILL[d.band]+'">'+BANDLAB[d.band]+'</span></td><td>'+(esc(d.text)||'(detail)')+' <span style="color:var(--mut);font-size:11px">detail</span></td><td>'+esc(d.sheet)+'</td><td>—</td><td><span class=hint>'+esc(d.reason)+'</span></td><td></td></tr>';});
|
|
889
|
+
if(!mem.length&&!det.length)h+='<tr><td colspan=6><div class=hint style="padding:14px 0">Nothing matches this filter.</div></td></tr>';
|
|
890
|
+
h+='</tbody></table>';return h;}
|
|
891
|
+
function wireConf(){
|
|
892
|
+
document.querySelectorAll('#confCats .ccard.click').forEach(c=>c.onclick=()=>{confCat=(confCat===c.dataset.cat?'all':c.dataset.cat);renderConf();});
|
|
893
|
+
document.querySelectorAll('#confFilter [data-band]').forEach(b=>b.onclick=()=>{confBand=b.dataset.band;renderConf();});
|
|
894
|
+
document.querySelectorAll('#confFilter [data-fcat]').forEach(b=>b.onclick=()=>{confCat=b.dataset.fcat;renderConf();});
|
|
895
|
+
document.querySelectorAll('#confBody tr.confrow').forEach(r=>r.onclick=e=>{if(e.target.closest('button'))return;const id=r.dataset.mid;confExpand=confExpand===id?null:id;renderConf();});
|
|
896
|
+
document.querySelectorAll('#confBody button[data-loc]').forEach(b=>b.onclick=e=>{e.stopPropagation();const pi=+b.dataset.pi;if(pi!==C.active)setPlan(pi);const m=byId(b.dataset.loc);if(!m)return;selIds=new Set([m.id]);geoMode=null;setGeo();closeConf();render();zoomToMember(m);});}
|
|
897
|
+
document.getElementById('confStat').onclick=openConf;
|
|
898
|
+
document.getElementById('confClose').onclick=closeConf;
|
|
899
|
+
document.getElementById('confBackdrop').onclick=closeConf;
|
|
783
900
|
// --- detail preview lightbox (click a card / an end's ⤢ to enlarge) ---
|
|
784
901
|
const SHEET_PREV=C.sheet_previews||{}, DBUB=C.detail_bubbles||{};
|
|
785
902
|
function sheetOf(text){const m=/(S-?\w+)/.exec(String(text||'').replace(/^\w+-/,''));if(!m)return null;const s=m[1].toUpperCase();return s.startsWith('S-')?s:'S-'+s.slice(1);}
|