@floless/app 0.74.0 → 0.76.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 +746 -270
- package/dist/schemas/steel.takeoff.v1.schema.json +26 -7
- package/dist/web/app.css +21 -2
- package/dist/web/app.js +13 -1
- package/dist/web/aware.js +16 -3
- package/dist/web/index.html +9 -0
- package/dist/web/steel-3d-view.js +17 -1
- package/dist/web/steel-editor.html +247 -14
- package/dist/web/workspaces.js +196 -9
- package/package.json +1 -1
|
@@ -236,19 +236,38 @@
|
|
|
236
236
|
},
|
|
237
237
|
"joint": {
|
|
238
238
|
"type": "object",
|
|
239
|
-
"required": ["id", "kind"
|
|
239
|
+
"required": ["id", "kind"],
|
|
240
240
|
"additionalProperties": true,
|
|
241
|
-
"description": "A placed connection
|
|
241
|
+
"description": "A placed connection the connection engine expands into real 3D parts. A parametric RECIPE kind (base-plate, shear-plate) derives all geometry from its `params` + the live member positions, so it moves/re-solves with its members. A 'custom' kind is an imported connection (read from IFC via AWARE's connection-reader) that carries its own opaque tessellated `geometry` and a `place` point — move / replace only, no per-dimension edit. Distinct from the `connections[]` library (vendor-neutral type/detail/targets identities a joint can cite). v1 kinds: base-plate, shear-plate, custom.",
|
|
242
242
|
"properties": {
|
|
243
|
-
"id": { "type": "string", "description": "Stable joint id, e.g. j-bp-1 / sp-b3-e0
|
|
244
|
-
"kind": { "type": "string", "description": "Connection kind
|
|
245
|
-
"main": { "type": "string", "description": "Id of the host member this joint hangs off. For a base-plate this is the column; for a shear-plate this is the SUPPORTED BEAM (the joint sits at one of the beam's ends — see `at`)." },
|
|
243
|
+
"id": { "type": "string", "description": "Stable joint id, e.g. j-bp-1 / sp-b3-e0 / cx-<id>." },
|
|
244
|
+
"kind": { "type": "string", "description": "Connection kind: base-plate, shear-plate (parametric recipes the engine expands), or custom (imported IFC geometry). end-plate / cap-plate / … follow." },
|
|
245
|
+
"main": { "type": "string", "description": "Id of the host member this joint hangs off. For a base-plate this is the column; for a shear-plate this is the SUPPORTED BEAM (the joint sits at one of the beam's ends — see `at`). Required for the recipe kinds; optional for 'custom' (an imported connection carries its own geometry rather than deriving it from a member)." },
|
|
246
246
|
"secondaries": { "type": "array", "items": { "type": "string" }, "description": "Ids of the other members this joint binds — e.g. the SUPPORT a shear-plate frames into. Optional and inferred by geometry; the shear-plate engine does not require it (the fin plate is placed off the beam end + beam axis). Empty for a base-plate." },
|
|
247
247
|
"at": { "type": "string", "description": "Where on the main member the joint sits (kind-dependent): base-plate = base; shear-plate = end0 | end1 — the beam end framing into the support (end0 ↔ ends[0]/start, end1 ↔ ends[1]/end)." },
|
|
248
248
|
"conn": { "type": "string", "description": "Optional `connections[]` row id this joint cites for its type/detail identity (keeps the connection library + confidence scoring in play)." },
|
|
249
|
-
"params": { "type": "object", "additionalProperties": true, "description": "Geometry settings (mm) with engine defaults; absent = all defaults. base-plate: plateWidth/plateDepth/thickness, boltCols/boltRows/boltDia/edgeDist, weldLeg, anchor kit, levelingNut. shear-plate: plateThickness, plateWidth (along the beam), plateHeight (vertical), boltCols/boltRows/boltDia/boltPitch/edgeDist, weldLeg, clearance (the erection gap so the beam clears the support, ~½–¾\"; pulls the beam box back), webSide (+1/-1 — which face of the beam web the plate laps), boltGrade (A325 | A490 — the AISC bolt length auto-sizes from the grip), stiffener (bool — a full-height stiffener on the far side of a girder web)." },
|
|
249
|
+
"params": { "type": "object", "additionalProperties": true, "description": "Geometry settings (mm) with engine defaults; absent = all defaults. base-plate: plateWidth/plateDepth/thickness, boltCols/boltRows/boltDia/edgeDist, weldLeg, anchor kit, levelingNut. shear-plate: plateThickness, plateWidth (along the beam), plateHeight (vertical), boltCols/boltRows/boltDia/boltPitch/edgeDist, weldLeg, clearance (the erection gap so the beam clears the support, ~½–¾\"; pulls the beam box back), webSide (+1/-1 — which face of the beam web the plate laps), boltGrade (A325 | A490 — the AISC bolt length auto-sizes from the grip), stiffener (bool — a full-height stiffener on the far side of a girder web). Not used by 'custom'." },
|
|
250
250
|
"source": { "enum": ["auto", "user"], "description": "auto = proposed by auto-detail from the takeoff; user = placed/edited by hand." },
|
|
251
|
-
"verified": { "type": "boolean", "description": "True = a human confirmed this joint." }
|
|
251
|
+
"verified": { "type": "boolean", "description": "True = a human confirmed this joint." },
|
|
252
|
+
"name": { "type": "string", "description": "custom only: display label carried from the source IFC (e.g. \"COLUMN C102\")." },
|
|
253
|
+
"place": { "$ref": "#/$defs/point3", "description": "custom only: the world-mm point the connection's LOCAL `geometry` is translated by — its placement in the model." },
|
|
254
|
+
"geometry": { "type": "array", "items": { "$ref": "#/$defs/meshPart" }, "description": "custom only: imported mesh parts (one per plate/bolt/weld) in the connection's LOCAL mm frame. Read once from IFC by AWARE's connection-reader; opaque tessellated geometry (no per-dimension edit)." }
|
|
255
|
+
},
|
|
256
|
+
"allOf": [
|
|
257
|
+
{ "if": { "properties": { "kind": { "enum": ["base-plate", "shear-plate"] } } }, "then": { "required": ["main"] } },
|
|
258
|
+
{ "if": { "properties": { "kind": { "const": "custom" } } }, "then": { "required": ["geometry"] } }
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
"meshPart": {
|
|
262
|
+
"type": "object",
|
|
263
|
+
"required": ["positions", "indices"],
|
|
264
|
+
"additionalProperties": true,
|
|
265
|
+
"description": "One tessellated mesh part of a 'custom' connection (a plate / bolt / weld), in the connection's LOCAL mm frame: flat x,y,z position triples + 0-based triangle indices — AWARE's generic kind:\"mesh\" scene primitive. Read from IFC by the connection-reader; never hand-authored.",
|
|
266
|
+
"properties": {
|
|
267
|
+
"id": { "type": "string", "description": "Source part id (the IFC element GlobalId)." },
|
|
268
|
+
"role": { "type": "string", "description": "Hardware role from the source IFC: plate | bolt | weld." },
|
|
269
|
+
"positions": { "type": "array", "items": { "type": "number" }, "description": "Flat x,y,z vertex coordinates (mm), local to the connection; length is a multiple of 3." },
|
|
270
|
+
"indices": { "type": "array", "items": { "type": "number" }, "description": "0-based triangle vertex indices (triples) into positions." }
|
|
252
271
|
}
|
|
253
272
|
},
|
|
254
273
|
"point2": {
|
package/dist/web/app.css
CHANGED
|
@@ -3344,6 +3344,8 @@ body {
|
|
|
3344
3344
|
.app.mode-workspaces .notes-strip, .app.mode-workspaces .find-overlay,
|
|
3345
3345
|
/* the ws surfaces carry their own headings — hide the canvas's "Canvas · transparency layer" label */
|
|
3346
3346
|
.app.mode-workspaces .canvas > .panel-label { display:none !important; }
|
|
3347
|
+
/* …but Find in model reuses that overlay — un-hide it while open (Ctrl+F searches editor members). */
|
|
3348
|
+
.app.mode-workspaces .find-overlay.show { display: flex !important; }
|
|
3347
3349
|
/* the editor owns center+right in a project — collapse the inspect column entirely */
|
|
3348
3350
|
.app.mode-workspaces { --right-width:0px !important; }
|
|
3349
3351
|
.app.mode-workspaces .inspect { display:none; }
|
|
@@ -3473,9 +3475,26 @@ table.hist tr.current td { background:var(--accent-soft); }
|
|
|
3473
3475
|
.hist .gate-badge { display:inline-block; padding:2px 7px; border-radius:4px; font-size:9.5px; font-weight:600;
|
|
3474
3476
|
text-transform:uppercase; letter-spacing:.05em; background:transparent; border:1px solid var(--border-strong); color:var(--text-dim); }
|
|
3475
3477
|
.hist .gate-badge.model { background:var(--accent-soft); border-color:var(--accent-dim); color:var(--accent-bright); }
|
|
3476
|
-
.hist .btn-mini { background:none; border:1px solid var(--border-strong); color:var(--text-muted); border-radius:6px;
|
|
3478
|
+
.hist .btn-mini, .ws-drawings-bar .btn-mini, .ws-revision-card .btn-mini { background:none; border:1px solid var(--border-strong); color:var(--text-muted); border-radius:6px;
|
|
3477
3479
|
padding:4px 10px; font-size:11px; cursor:pointer; white-space:nowrap; }
|
|
3478
|
-
.hist .btn-mini:hover { color:var(--text); border-color:var(--accent); }
|
|
3480
|
+
.hist .btn-mini:hover, .ws-drawings-bar .btn-mini:hover, .ws-revision-card .btn-mini:hover { color:var(--text); border-color:var(--accent); }
|
|
3481
|
+
|
|
3482
|
+
/* ── Workspaces: revision re-read (Slice 4b) — attach band + pending/failed card ── */
|
|
3483
|
+
.ws-drawings-bar { flex:none; display:flex; align-items:center; gap:10px; padding:8px 14px; border-bottom:1px solid var(--border); background:var(--surface); }
|
|
3484
|
+
.ws-drawings-bar[hidden] { display:none !important; }
|
|
3485
|
+
.ws-drawings-bar-label { font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.1em; }
|
|
3486
|
+
.ws-drawings-bar-sep { flex:1; }
|
|
3487
|
+
.ws-revision-card { flex:none; display:flex; align-items:center; gap:10px; padding:9px 14px; border-bottom:1px solid var(--border); background:var(--surface-2); font-size:12px; color:var(--text-muted); }
|
|
3488
|
+
.ws-revision-card[hidden] { display:none !important; }
|
|
3489
|
+
.ws-revision-card b { color:var(--text); font-weight:600; }
|
|
3490
|
+
.ws-revision-card .wrc-text { flex:1; min-width:0; }
|
|
3491
|
+
.ws-revision-card .wrc-spin { flex:none; width:14px; height:14px; border:2px solid var(--border-strong); border-top-color:var(--accent); border-radius:50%; animation:spin .8s linear infinite; }
|
|
3492
|
+
.ws-revision-card.state-failed { border-left:3px solid var(--err); }
|
|
3493
|
+
.ws-revision-card.state-failed .wrc-err-ico { color:var(--err); flex:none; }
|
|
3494
|
+
.ws-revision-card.state-failed .wrc-err-detail { color:var(--text-dim); }
|
|
3495
|
+
.ws-revision-card .wrc-actions { display:flex; gap:6px; flex:none; }
|
|
3496
|
+
@media (prefers-reduced-motion: reduce) { .ws-revision-card .wrc-spin { animation:none; } }
|
|
3497
|
+
.hist .gate-badge.ai-read { background:var(--accent-soft); border-color:var(--accent-dim); color:var(--accent-bright); }
|
|
3479
3498
|
|
|
3480
3499
|
/* Rollback confirm — a styled anchored popover (NEVER a native dialog); names the Exports re-lock. */
|
|
3481
3500
|
.hist-confirm { position:absolute; z-index:20; width:264px; padding:12px 13px; background:var(--surface-3);
|
package/dist/web/app.js
CHANGED
|
@@ -1094,6 +1094,10 @@ const $findInput = document.getElementById('find-input');
|
|
|
1094
1094
|
const $findCount = document.getElementById('find-count');
|
|
1095
1095
|
|
|
1096
1096
|
function openFind() {
|
|
1097
|
+
const ws = window.flolessWorkspaces && window.flolessWorkspaces.findActive();
|
|
1098
|
+
// In Workspaces mode there's nothing to find off the Model step — don't pop a dead overlay.
|
|
1099
|
+
if (document.getElementById('app').classList.contains('mode-workspaces') && !ws) return;
|
|
1100
|
+
$findInput.placeholder = ws ? 'Find in model…' : 'Find agent…';
|
|
1097
1101
|
$findOverlay.classList.add('show');
|
|
1098
1102
|
$findInput.value = '';
|
|
1099
1103
|
$findCount.textContent = '';
|
|
@@ -1101,13 +1105,21 @@ function openFind() {
|
|
|
1101
1105
|
}
|
|
1102
1106
|
function closeFind() {
|
|
1103
1107
|
$findOverlay.classList.remove('show');
|
|
1108
|
+
if (window.flolessWorkspaces && window.flolessWorkspaces.clearFind) window.flolessWorkspaces.clearFind();
|
|
1104
1109
|
document.querySelectorAll('.agent-card').forEach(c => {
|
|
1105
1110
|
c.classList.remove('find-dim', 'find-match');
|
|
1106
1111
|
});
|
|
1107
1112
|
}
|
|
1108
1113
|
|
|
1109
1114
|
$findInput.addEventListener('input', () => {
|
|
1110
|
-
const
|
|
1115
|
+
const raw = $findInput.value;
|
|
1116
|
+
const q = raw.toLowerCase().trim();
|
|
1117
|
+
// Workspaces + Model step: search members in the embedded editor, not the canvas.
|
|
1118
|
+
if (window.flolessWorkspaces && window.flolessWorkspaces.findActive()) {
|
|
1119
|
+
const r = window.flolessWorkspaces.find(raw) || { count: 0 };
|
|
1120
|
+
$findCount.textContent = q ? `${r.count} match${r.count === 1 ? '' : 'es'}` : '';
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1111
1123
|
let matches = 0;
|
|
1112
1124
|
document.querySelectorAll('.agent-card').forEach(card => {
|
|
1113
1125
|
if (!q) {
|
package/dist/web/aware.js
CHANGED
|
@@ -4655,6 +4655,13 @@
|
|
|
4655
4655
|
if (req.type === 'new-workflow') {
|
|
4656
4656
|
return `Help me build a brand-new floless workflow from scratch, step by step, with your floless-app-new-workflow skill. Ask me what I want it to do, then before writing any node confirm the agents it needs are installed (offer to install a missing one from the catalogue, or to report a not-yet-existing agent as an idea). Author the .flo node by node, verifying each step is correct (plain-English descriptions, validate, compile, a real run), and finish by installing → Compile → ▶ Run so I can see it work and approve.`;
|
|
4657
4657
|
}
|
|
4658
|
+
if (req.type === 'revision-read') {
|
|
4659
|
+
const snaps = req.snapshots && req.snapshots.length
|
|
4660
|
+
? `\nRevised drawing${req.snapshots.length > 1 ? 's' : ''} (read these): ${req.snapshots.join(', ')}`
|
|
4661
|
+
: '';
|
|
4662
|
+
const note = req.instruction ? `\nMy note: ${req.instruction}` : '';
|
|
4663
|
+
return `In floless project "${req.project}" (app "${req.appId}"), a REVISED drawing set was attached — re-read it into the SAME project as a new version (do NOT create a new project). Read the drawings per the floless-app-steel-takeoff skill, compose the updated steel.takeoff/v1 contract, then POST /api/projects/${req.project}/revision-read with { contract, message: "<plain-English what changed>", requestId: "${req.id}" }. The server derives the base version + source provenance from this queued request, records a "revision-read" version, and clears the request — do NOT DELETE the request yourself. If you can't read the set, POST /api/projects/${req.project}/revision-requests/${req.id}/fail with { error }.${snaps}${note}`;
|
|
4664
|
+
}
|
|
4658
4665
|
return '';
|
|
4659
4666
|
}
|
|
4660
4667
|
|
|
@@ -4665,6 +4672,7 @@
|
|
|
4665
4672
|
tweak: 'floless-app-workflows',
|
|
4666
4673
|
'ui-customize': 'floless-app-ui',
|
|
4667
4674
|
rebake: 'floless-app-rebake',
|
|
4675
|
+
'revision-read': 'floless-app-steel-takeoff',
|
|
4668
4676
|
// The guided-tour ask is picked up by onboarding; the instruction body names the
|
|
4669
4677
|
// per-app floless-app-<appId> skill for depth when one exists.
|
|
4670
4678
|
guide: 'floless-app-onboarding',
|
|
@@ -4682,10 +4690,15 @@
|
|
|
4682
4690
|
if (!body) return '';
|
|
4683
4691
|
const skill = REQUEST_SKILL[req.type] || 'floless-app-workflows';
|
|
4684
4692
|
const base = (typeof location !== 'undefined' && location.origin) ? location.origin : 'http://127.0.0.1:4317';
|
|
4693
|
+
// A revision-read is cleared by its own /revision-read POST — a separate DELETE would 404. Every
|
|
4694
|
+
// other type is applied-then-DELETEd by the terminal AI.
|
|
4695
|
+
const clears = req.type === 'revision-read'
|
|
4696
|
+
? `apply it via the route in the line below (that POST clears this request — do NOT DELETE it separately).`
|
|
4697
|
+
: `apply that request, then DELETE ${base}/api/requests/${req.id}.`;
|
|
4685
4698
|
const marker =
|
|
4686
4699
|
`[floless-request type=${req.type} id=${req.id}] — queued from the FloLess Dashboard. ` +
|
|
4687
4700
|
`Apply it with your ${skill} skill: fetch the authoritative spec from GET ${base}/api/requests, ` +
|
|
4688
|
-
|
|
4701
|
+
`${clears} Don't run the line below verbatim.`;
|
|
4689
4702
|
return `${marker}\n${body}`;
|
|
4690
4703
|
}
|
|
4691
4704
|
|
|
@@ -4764,8 +4777,8 @@
|
|
|
4764
4777
|
return;
|
|
4765
4778
|
}
|
|
4766
4779
|
$list.innerHTML = pendingRequests.map((r) => {
|
|
4767
|
-
const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : r.type === 'rebake' ? 're-bake' : r.type === 'guide' ? 'guide' : r.type === 'new-workflow' ? 'new workflow' : 'tweak';
|
|
4768
|
-
const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' || r.type === 'rebake' ? 'req-type req-type-tweak' : 'req-type';
|
|
4780
|
+
const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : r.type === 'rebake' ? 're-bake' : r.type === 'revision-read' ? 'revision' : r.type === 'guide' ? 'guide' : r.type === 'new-workflow' ? 'new workflow' : 'tweak';
|
|
4781
|
+
const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' || r.type === 'rebake' || r.type === 'revision-read' ? 'req-type req-type-tweak' : 'req-type';
|
|
4769
4782
|
const target = r.type === 'tweak' && r.nodeId
|
|
4770
4783
|
? ` · node <code>${escapeHtml(r.nodeId)}</code>`
|
|
4771
4784
|
: r.type === 'rebake' && r.inputName
|
package/dist/web/index.html
CHANGED
|
@@ -170,6 +170,15 @@
|
|
|
170
170
|
<button type="button" data-step="exports" role="tab" aria-selected="false">Exports</button>
|
|
171
171
|
<button type="button" data-step="history" role="tab" aria-selected="false">History</button>
|
|
172
172
|
</div>
|
|
173
|
+
<!-- Drawings step: a shell toolbar band + revision-read status card ABOVE the filter iframe
|
|
174
|
+
(chrome around it, never inside — the filter owns its own layout). Shown only on Drawings. -->
|
|
175
|
+
<div class="ws-drawings-bar" id="ws-drawings-bar" hidden>
|
|
176
|
+
<span class="ws-drawings-bar-label">Drawing set</span>
|
|
177
|
+
<span class="ws-drawings-bar-sep"></span>
|
|
178
|
+
<button type="button" id="ws-attach-revision" class="btn-mini" data-tip="Attach a revised drawing set — your terminal AI reads it into a new version">⤒ Attach revised drawings…</button>
|
|
179
|
+
<input type="file" id="ws-revision-file" accept=".pdf,image/*" multiple hidden>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="ws-revision-card" id="ws-revision-card" hidden></div>
|
|
173
182
|
<!-- Two lazily-srced iframes so switching steps never reloads the editor's 3D state.
|
|
174
183
|
Same-origin like #contract-editor-frame (they call /api/contract directly). -->
|
|
175
184
|
<iframe id="ws-frame-model" class="ws-frame" title="Project model editor" hidden></iframe>
|
|
@@ -422,6 +422,18 @@ function placeWasher(mesh, el) {
|
|
|
422
422
|
mesh.position.copy(A); // extrudes from `from` toward `to`
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
// An imported connection part (slice B): AWARE's generic kind:"mesh" primitive — flat world-mm
|
|
426
|
+
// positions + 0-based triangle indices straight from the connection-reader. Absolute coordinates, so
|
|
427
|
+
// the mesh sits at the root origin like a member box (no local transform). Winding order isn't trusted
|
|
428
|
+
// (its material is double-sided) — computeVertexNormals so it still shades.
|
|
429
|
+
function placeMesh(mesh, el) {
|
|
430
|
+
const g = new THREE.BufferGeometry();
|
|
431
|
+
g.setAttribute('position', new THREE.Float32BufferAttribute(el.positions || [], 3));
|
|
432
|
+
if (Array.isArray(el.indices) && el.indices.length) g.setIndex(el.indices);
|
|
433
|
+
g.computeVertexNormals();
|
|
434
|
+
mesh.geometry = g;
|
|
435
|
+
}
|
|
436
|
+
|
|
425
437
|
// Build a mesh for one scene element by kind: a member box (default) or a connection part. Returns
|
|
426
438
|
// false for an unknown kind so the caller skips it (forward-compatible with later part kinds).
|
|
427
439
|
function placeElement(mesh, el) {
|
|
@@ -434,6 +446,7 @@ function placeElement(mesh, el) {
|
|
|
434
446
|
case 'nut': placeNut(mesh, el); return true;
|
|
435
447
|
case 'washer': placeWasher(mesh, el); return true;
|
|
436
448
|
case 'weld': placeWeld(mesh, el); return true;
|
|
449
|
+
case 'mesh': placeMesh(mesh, el); return true;
|
|
437
450
|
default: return false;
|
|
438
451
|
}
|
|
439
452
|
}
|
|
@@ -444,7 +457,10 @@ function materialFor(groupKey) {
|
|
|
444
457
|
// Fasteners (anchor rods / bolts / nuts / washers) read more metallic (zinc-plated); plates/welds/members stay matte.
|
|
445
458
|
const metallic = groupKey === 'anchor' || groupKey === 'bolt' || groupKey === 'nut' || groupKey === 'washer';
|
|
446
459
|
const metal = metallic ? 0.5 : 0.1, rough = metallic ? 0.4 : 0.75;
|
|
447
|
-
|
|
460
|
+
// Imported (custom) connection meshes are tessellated with untrusted winding order → render both
|
|
461
|
+
// faces so no triangle drops out; matte steel like a plate.
|
|
462
|
+
const dbl = groupKey === 'custom';
|
|
463
|
+
baseMat.set(groupKey, new THREE.MeshStandardMaterial({ color: col, metalness: metal, roughness: rough, side: dbl ? THREE.DoubleSide : THREE.FrontSide }));
|
|
448
464
|
}
|
|
449
465
|
return baseMat.get(groupKey);
|
|
450
466
|
}
|