@ifc-lite/viewer 1.26.0 → 1.28.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/.turbo/turbo-build.log +45 -38
- package/CHANGELOG.md +93 -0
- package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
- package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/deflate-DNGgs8Ur.js +1 -0
- package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
- package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
- package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
- package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
- package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
- package/dist/assets/index-BtbXFKsX.css +1 -0
- package/dist/assets/index.es-CWfqZyyr.js +6866 -0
- package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
- package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
- package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
- package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
- package/dist/assets/pdf-CRwaZf3s.js +135 -0
- package/dist/assets/raw-CJgQdyuZ.js +1 -0
- package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
- package/dist/assets/server-client-cTCJ-853.js +719 -0
- package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
- package/dist/assets/xlsx-B1YOg2QB.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
- package/dist/index.html +10 -10
- package/package.json +27 -23
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +280 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +46 -7
- package/src/components/viewer/MainToolbar.tsx +19 -2
- package/src/components/viewer/PropertiesPanel.tsx +84 -8
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +18 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +481 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +1 -26
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +23 -1
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +32 -9
- package/src/hooks/useIfcFederation.ts +42 -810
- package/src/hooks/useIfcLoader.ts +361 -488
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/search/result-export.ts +7 -1
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +9 -4
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +16 -1
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/desktopModelSnapshot.ts +2 -1
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/deflate-Cnx0il6E.js +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-8md211IW.js +0 -182
- package/dist/assets/raw-BQrAgxwT.js +0 -1
- package/dist/assets/server-client-Bk4c1CPO.js +0 -626
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Model length-unit → metres scale, memoised per `IfcDataStore`.
|
|
7
|
+
*
|
|
8
|
+
* The viewer's render + authoring space is **metres**: the geometry
|
|
9
|
+
* pipeline bakes the file's length-unit scale into tessellated vertices,
|
|
10
|
+
* raycast hit-points come back in metres, and `spatialHierarchy.
|
|
11
|
+
* storeyElevations` are pre-scaled. But raw coordinate reads straight off
|
|
12
|
+
* the STEP model — split footprints, placement chains — arrive in the
|
|
13
|
+
* file's **native** units (e.g. millimetres). Multiply those by this
|
|
14
|
+
* factor to bring them into the same metre space as everything else.
|
|
15
|
+
*
|
|
16
|
+
* Returns `1` when the scale can't be determined (already-metres models,
|
|
17
|
+
* or bounded-geometry mode having released the source buffer) — the
|
|
18
|
+
* safe identity that leaves native-unit reads untouched.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
22
|
+
|
|
23
|
+
const scaleCache = new WeakMap<IfcDataStore, number>();
|
|
24
|
+
|
|
25
|
+
export function getModelLengthUnitScale(dataStore: IfcDataStore | null | undefined): number {
|
|
26
|
+
if (!dataStore) return 1;
|
|
27
|
+
const cached = scaleCache.get(dataStore);
|
|
28
|
+
if (cached !== undefined) return cached;
|
|
29
|
+
|
|
30
|
+
// The columnar parser stashes the scale on the store; the wasm fast
|
|
31
|
+
// path does not, so fall back to extracting it from the source bytes.
|
|
32
|
+
let scale = typeof dataStore.lengthUnitScale === 'number' ? dataStore.lengthUnitScale : undefined;
|
|
33
|
+
if (scale === undefined || !Number.isFinite(scale) || scale <= 0) {
|
|
34
|
+
if (!dataStore.source?.length || !dataStore.entityIndex) return 1;
|
|
35
|
+
scale = extractLengthUnitScale(dataStore.source, dataStore.entityIndex);
|
|
36
|
+
}
|
|
37
|
+
if (!Number.isFinite(scale) || scale <= 0) scale = 1;
|
|
38
|
+
|
|
39
|
+
scaleCache.set(dataStore, scale);
|
|
40
|
+
return scale;
|
|
41
|
+
}
|
package/src/lib/lists/adapter.ts
CHANGED
|
@@ -11,10 +11,31 @@
|
|
|
11
11
|
* and Tag which are not stored during the fast initial parse.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
15
|
-
import {
|
|
14
|
+
import type { IfcDataStore, MaterialInfo } from '@ifc-lite/parser';
|
|
15
|
+
import {
|
|
16
|
+
extractPropertiesOnDemand,
|
|
17
|
+
extractQuantitiesOnDemand,
|
|
18
|
+
extractEntityAttributesOnDemand,
|
|
19
|
+
extractMaterialsOnDemand,
|
|
20
|
+
extractClassificationsOnDemand,
|
|
21
|
+
} from '@ifc-lite/parser';
|
|
16
22
|
import type { PropertySet, QuantitySet } from '@ifc-lite/data';
|
|
17
|
-
import
|
|
23
|
+
import { ENTITY_ATTRIBUTES } from '@ifc-lite/lists';
|
|
24
|
+
import type { ListDataProvider, ListClassificationRef, DiscoveredColumns } from '@ifc-lite/lists';
|
|
25
|
+
|
|
26
|
+
/** Collect every material-name string an element exposes — top-level
|
|
27
|
+
* material plus layer / constituent / profile names and list members. */
|
|
28
|
+
function materialNamesOf(info: MaterialInfo | null): string[] {
|
|
29
|
+
if (!info) return [];
|
|
30
|
+
const names: string[] = [];
|
|
31
|
+
const push = (s: string | undefined) => { if (s) names.push(s); };
|
|
32
|
+
push(info.name);
|
|
33
|
+
for (const l of info.layers ?? []) { push(l.materialName); push(l.name); }
|
|
34
|
+
for (const c of info.constituents ?? []) { push(c.materialName); push(c.name); }
|
|
35
|
+
for (const p of info.profiles ?? []) { push(p.materialName); push(p.name); }
|
|
36
|
+
for (const m of info.materials ?? []) push(m.name);
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
18
39
|
|
|
19
40
|
/**
|
|
20
41
|
* Create a ListDataProvider backed by an IfcDataStore.
|
|
@@ -26,6 +47,10 @@ export function createListDataProvider(store: IfcDataStore): ListDataProvider {
|
|
|
26
47
|
// but are needed for list display. Cache avoids re-parsing per column.
|
|
27
48
|
const attrCache = new Map<number, { description: string; objectType: string; tag: string }>();
|
|
28
49
|
|
|
50
|
+
// Lazily materialised list of every non-empty express id — used for
|
|
51
|
+
// class-less list targeting. Cached because the provider outlives a run.
|
|
52
|
+
let allIdsCache: number[] | null = null;
|
|
53
|
+
|
|
29
54
|
function getOnDemandAttrs(id: number): { description: string; objectType: string; tag: string } {
|
|
30
55
|
const cached = attrCache.get(id);
|
|
31
56
|
if (cached) return cached;
|
|
@@ -42,6 +67,23 @@ export function createListDataProvider(store: IfcDataStore): ListDataProvider {
|
|
|
42
67
|
return empty;
|
|
43
68
|
}
|
|
44
69
|
|
|
70
|
+
// Complete column discovery is cached — the provider outlives a builder
|
|
71
|
+
// open, and the scan touches every entity that declares a pset/qto.
|
|
72
|
+
let columnsCache: DiscoveredColumns | null = null;
|
|
73
|
+
|
|
74
|
+
const usesOnDemandProps = !!store.onDemandPropertyMap && store.source?.length > 0;
|
|
75
|
+
const usesOnDemandQtos = !!store.onDemandQuantityMap && store.source?.length > 0;
|
|
76
|
+
|
|
77
|
+
function getPropertySetsFor(entityId: number): PropertySet[] {
|
|
78
|
+
if (usesOnDemandProps) return extractPropertiesOnDemand(store, entityId) as PropertySet[];
|
|
79
|
+
return store.properties?.getForEntity(entityId) ?? [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getQuantitySetsFor(entityId: number): QuantitySet[] {
|
|
83
|
+
if (usesOnDemandQtos) return extractQuantitiesOnDemand(store, entityId) as QuantitySet[];
|
|
84
|
+
return store.quantities?.getForEntity(entityId) ?? [];
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
return {
|
|
46
88
|
getEntitiesByType: (type) => store.entities.getByType(type),
|
|
47
89
|
|
|
@@ -52,18 +94,101 @@ export function createListDataProvider(store: IfcDataStore): ListDataProvider {
|
|
|
52
94
|
getEntityTag: (id) => getOnDemandAttrs(id).tag,
|
|
53
95
|
getEntityTypeName: (id) => store.entities.getTypeName(id),
|
|
54
96
|
|
|
55
|
-
getPropertySets
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
getPropertySets: getPropertySetsFor,
|
|
98
|
+
getQuantitySets: getQuantitySetsFor,
|
|
99
|
+
|
|
100
|
+
getAllEntityIds(): number[] {
|
|
101
|
+
if (allIdsCache) return allIdsCache;
|
|
102
|
+
// Restrict "all elements" to geometry-bearing (selectable) products.
|
|
103
|
+
// The raw expressId column also holds relationships, property sets,
|
|
104
|
+
// materials, classifications and other non-element records — a
|
|
105
|
+
// class-less list should not surface those as rows.
|
|
106
|
+
const ids: number[] = [];
|
|
107
|
+
const col = store.entities.expressId;
|
|
108
|
+
for (let i = 0; i < col.length; i++) {
|
|
109
|
+
const id = col[i];
|
|
110
|
+
if (id && store.entities.hasGeometry(id)) ids.push(id);
|
|
58
111
|
}
|
|
59
|
-
|
|
112
|
+
allIdsCache = ids;
|
|
113
|
+
return ids;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
getMaterialNames(entityId: number): string[] {
|
|
117
|
+
return materialNamesOf(extractMaterialsOnDemand(store, entityId));
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
getClassifications(entityId: number): ListClassificationRef[] {
|
|
121
|
+
return extractClassificationsOnDemand(store, entityId).map((c) => ({
|
|
122
|
+
system: c.system,
|
|
123
|
+
code: c.identification,
|
|
124
|
+
name: c.name,
|
|
125
|
+
}));
|
|
60
126
|
},
|
|
61
127
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
128
|
+
getStoreyName(entityId: number): string {
|
|
129
|
+
const hierarchy = store.spatialHierarchy;
|
|
130
|
+
if (!hierarchy) return '';
|
|
131
|
+
const storeyId = hierarchy.elementToStorey.get(entityId);
|
|
132
|
+
if (!storeyId) return '';
|
|
133
|
+
return store.entities.getName(storeyId) || '';
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
discoverAllColumns(): DiscoveredColumns {
|
|
137
|
+
if (columnsCache) return columnsCache;
|
|
138
|
+
|
|
139
|
+
const properties = new Map<string, Set<string>>();
|
|
140
|
+
const quantities = new Map<string, Set<string>>();
|
|
141
|
+
|
|
142
|
+
const ingestProps = (id: number) => {
|
|
143
|
+
for (const set of getPropertySetsFor(id)) {
|
|
144
|
+
if (!set.name) continue;
|
|
145
|
+
let bucket = properties.get(set.name);
|
|
146
|
+
if (!bucket) { bucket = new Set(); properties.set(set.name, bucket); }
|
|
147
|
+
for (const p of set.properties) if (p.name) bucket.add(p.name);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const ingestQtos = (id: number) => {
|
|
151
|
+
for (const set of getQuantitySetsFor(id)) {
|
|
152
|
+
if (!set.name) continue;
|
|
153
|
+
let bucket = quantities.get(set.name);
|
|
154
|
+
if (!bucket) { bucket = new Set(); quantities.set(set.name, bucket); }
|
|
155
|
+
for (const q of set.quantities) if (q.name) bucket.add(q.name);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// On-demand path: scan exactly the entities that declare a pset/qto —
|
|
160
|
+
// the minimal complete set (every distinct set/property in the model).
|
|
161
|
+
if (usesOnDemandProps && store.onDemandPropertyMap) {
|
|
162
|
+
for (const id of store.onDemandPropertyMap.keys()) ingestProps(id);
|
|
163
|
+
}
|
|
164
|
+
if (usesOnDemandQtos && store.onDemandQuantityMap) {
|
|
165
|
+
for (const id of store.onDemandQuantityMap.keys()) ingestQtos(id);
|
|
65
166
|
}
|
|
66
|
-
|
|
167
|
+
// Table path (e.g. server-loaded models): scan the entity column using
|
|
168
|
+
// the pre-built tables. Capped so it can't run away on huge models.
|
|
169
|
+
if (!usesOnDemandProps || !usesOnDemandQtos) {
|
|
170
|
+
const col = store.entities.expressId;
|
|
171
|
+
const CAP = 100_000;
|
|
172
|
+
for (let i = 0, seen = 0; i < col.length && seen < CAP; i++) {
|
|
173
|
+
const id = col[i];
|
|
174
|
+
if (!id) continue;
|
|
175
|
+
seen++;
|
|
176
|
+
if (!usesOnDemandProps) ingestProps(id);
|
|
177
|
+
if (!usesOnDemandQtos) ingestQtos(id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const toSorted = (m: Map<string, Set<string>>) => {
|
|
182
|
+
const out = new Map<string, string[]>();
|
|
183
|
+
for (const [k, s] of m) out.set(k, Array.from(s).sort());
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
columnsCache = {
|
|
187
|
+
attributes: [...ENTITY_ATTRIBUTES],
|
|
188
|
+
properties: toSorted(properties),
|
|
189
|
+
quantities: toSorted(quantities),
|
|
190
|
+
};
|
|
191
|
+
return columnsCache;
|
|
67
192
|
},
|
|
68
193
|
};
|
|
69
194
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { CellValue } from '@ifc-lite/lists';
|
|
6
|
+
import { displayCell, type ExportModel } from './model';
|
|
7
|
+
|
|
8
|
+
function esc(s: string, delim: string): string {
|
|
9
|
+
return /["\r\n]/.test(s) || s.includes(delim) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CSV faithful to the configured columns. When grouped, a leading "Group"
|
|
14
|
+
* column preserves the grouping as data (so it stays re-importable), rows are
|
|
15
|
+
* ordered by group, and a TOTAL row carries the grand count + sums.
|
|
16
|
+
*/
|
|
17
|
+
export function toCsv(model: ExportModel, delimiter = ','): string {
|
|
18
|
+
const grouped = model.groups !== null;
|
|
19
|
+
const header = [...(grouped ? ['Group'] : []), ...model.columns.map((c) => c.label)];
|
|
20
|
+
const lines = [header.map((h) => esc(h, delimiter)).join(delimiter)];
|
|
21
|
+
|
|
22
|
+
const line = (groupLabel: string | null, values: CellValue[]) => {
|
|
23
|
+
const cells = grouped ? [esc(groupLabel ?? '', delimiter)] : [];
|
|
24
|
+
for (let i = 0; i < model.columns.length; i++) cells.push(esc(displayCell(values[i]), delimiter));
|
|
25
|
+
return cells.join(delimiter);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (grouped && model.groups) {
|
|
29
|
+
for (const g of model.groups) for (const r of g.rows) lines.push(line(g.label, r));
|
|
30
|
+
} else {
|
|
31
|
+
for (const r of model.rows) lines.push(line(null, r));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (model.sumColumnIds.length > 0) {
|
|
35
|
+
const totalLabel = `TOTAL (${model.totals.count})`;
|
|
36
|
+
const cells = grouped ? [esc(totalLabel, delimiter)] : [];
|
|
37
|
+
for (let i = 0; i < model.columns.length; i++) {
|
|
38
|
+
const c = model.columns[i];
|
|
39
|
+
if (c.summed) cells.push(esc(displayCell(model.totals.sums[c.id]), delimiter));
|
|
40
|
+
else if (!grouped && i === 0) cells.push(esc(totalLabel, delimiter));
|
|
41
|
+
else cells.push('');
|
|
42
|
+
}
|
|
43
|
+
lines.push(cells.join(delimiter));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join('\r\n');
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List results export — CSV / Excel / PDF, all driven by one normalised model
|
|
7
|
+
* (columns, grouping, sums, totals). Excel and PDF writers (and their heavy
|
|
8
|
+
* libs) are lazy-loaded so they never touch the initial bundle.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { toCsv } from './csv';
|
|
12
|
+
import type { ExportModel } from './model';
|
|
13
|
+
|
|
14
|
+
export type ExportFormat = 'csv' | 'xlsx' | 'pdf';
|
|
15
|
+
|
|
16
|
+
export const EXPORT_LABELS: Record<ExportFormat, string> = {
|
|
17
|
+
csv: 'CSV (.csv)',
|
|
18
|
+
xlsx: 'Excel (.xlsx)',
|
|
19
|
+
pdf: 'PDF (.pdf)',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function slug(s: string): string {
|
|
23
|
+
return (s || 'list').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'list';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function download(blob: Blob, filename: string): void {
|
|
27
|
+
const url = URL.createObjectURL(blob);
|
|
28
|
+
const a = document.createElement('a');
|
|
29
|
+
a.href = url;
|
|
30
|
+
a.download = filename;
|
|
31
|
+
a.click();
|
|
32
|
+
setTimeout(() => URL.revokeObjectURL(url), 1500);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function exportList(format: ExportFormat, model: ExportModel): Promise<void> {
|
|
36
|
+
const name = slug(model.title);
|
|
37
|
+
if (format === 'csv') {
|
|
38
|
+
download(new Blob([toCsv(model)], { type: 'text/csv;charset=utf-8;' }), `${name}.csv`);
|
|
39
|
+
} else if (format === 'xlsx') {
|
|
40
|
+
const { toXlsx } = await import('./xlsx');
|
|
41
|
+
download(await toXlsx(model), `${name}.xlsx`);
|
|
42
|
+
} else {
|
|
43
|
+
const { toPdf } = await import('./pdf');
|
|
44
|
+
download(await toPdf(model), `${name}.pdf`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { buildExportModel } from './model';
|
|
49
|
+
export type { ExportModel } from './model';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalised export model shared by the CSV / Excel / PDF writers. Built from
|
|
7
|
+
* the on-screen list view so every export honours the configured columns
|
|
8
|
+
* (order, labels, widths), the active grouping, and the summed columns —
|
|
9
|
+
* grouped sections with per-group count + subtotals, plus grand totals.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CellValue, ColumnDefinition, ListRow, ListGrouping } from '@ifc-lite/lists';
|
|
13
|
+
|
|
14
|
+
export interface ExportColumn {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
numeric: boolean;
|
|
18
|
+
summed: boolean;
|
|
19
|
+
/** Pixel width from the table (for proportional column sizing in exports). */
|
|
20
|
+
width: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExportGroup {
|
|
24
|
+
label: string;
|
|
25
|
+
count: number;
|
|
26
|
+
sums: Record<string, number>;
|
|
27
|
+
rows: CellValue[][];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExportModel {
|
|
31
|
+
title: string;
|
|
32
|
+
generatedAt: string;
|
|
33
|
+
columns: ExportColumn[];
|
|
34
|
+
/** Grouped sections (with member rows), or null when the list isn't grouped. */
|
|
35
|
+
groups: ExportGroup[] | null;
|
|
36
|
+
/** All rows in display order (flat) — used by writers that don't section. */
|
|
37
|
+
rows: CellValue[][];
|
|
38
|
+
groupColumnId: string | null;
|
|
39
|
+
sumColumnIds: string[];
|
|
40
|
+
totals: { count: number; sums: Record<string, number> };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuildModelInput {
|
|
44
|
+
title: string;
|
|
45
|
+
columns: ColumnDefinition[];
|
|
46
|
+
/** Rows already filtered + sorted exactly as shown on screen. */
|
|
47
|
+
rows: ListRow[];
|
|
48
|
+
grouping?: ListGrouping;
|
|
49
|
+
numericCols: boolean[];
|
|
50
|
+
columnWidths: number[];
|
|
51
|
+
generatedAt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Format a cell for text-based exports (CSV/PDF). Excel keeps raw numbers. */
|
|
55
|
+
export function displayCell(value: CellValue): string {
|
|
56
|
+
if (value === null || value === undefined) return '';
|
|
57
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
58
|
+
if (typeof value === 'number') {
|
|
59
|
+
if (Number.isInteger(value)) return value.toLocaleString();
|
|
60
|
+
return value.toFixed(4).replace(/\.?0+$/, '');
|
|
61
|
+
}
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildExportModel(input: BuildModelInput): ExportModel {
|
|
66
|
+
const { columns, rows, grouping, numericCols, columnWidths, title, generatedAt } = input;
|
|
67
|
+
const sumColumnIds = grouping?.sumColumnIds ?? [];
|
|
68
|
+
const exportCols: ExportColumn[] = columns.map((c, i) => ({
|
|
69
|
+
id: c.id,
|
|
70
|
+
label: c.label ?? c.propertyName,
|
|
71
|
+
numeric: !!numericCols[i],
|
|
72
|
+
summed: sumColumnIds.includes(c.id),
|
|
73
|
+
width: columnWidths[i] ?? 120,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const sumIdx = sumColumnIds
|
|
77
|
+
.map((id) => ({ id, idx: columns.findIndex((c) => c.id === id) }))
|
|
78
|
+
.filter((s) => s.idx >= 0);
|
|
79
|
+
const zeroSums = (): Record<string, number> => Object.fromEntries(sumIdx.map((s) => [s.id, 0]));
|
|
80
|
+
const addSums = (acc: Record<string, number>, values: CellValue[]) => {
|
|
81
|
+
for (const s of sumIdx) {
|
|
82
|
+
const v = values[s.idx];
|
|
83
|
+
if (typeof v === 'number' && Number.isFinite(v)) acc[s.id] += v;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const totals = { count: rows.length, sums: zeroSums() };
|
|
88
|
+
const flatRows: CellValue[][] = [];
|
|
89
|
+
for (const r of rows) { flatRows.push(r.values); addSums(totals.sums, r.values); }
|
|
90
|
+
|
|
91
|
+
const groupColumnId = grouping?.columnId && columns.some((c) => c.id === grouping.columnId)
|
|
92
|
+
? grouping.columnId : null;
|
|
93
|
+
|
|
94
|
+
let groups: ExportGroup[] | null = null;
|
|
95
|
+
if (groupColumnId) {
|
|
96
|
+
const groupIdx = columns.findIndex((c) => c.id === groupColumnId);
|
|
97
|
+
const byKey = new Map<string, ExportGroup>();
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
const raw = r.values[groupIdx];
|
|
100
|
+
const label = raw === null || raw === undefined || raw === '' ? '(none)' : displayCell(raw);
|
|
101
|
+
let g = byKey.get(label);
|
|
102
|
+
if (!g) { g = { label, count: 0, sums: zeroSums(), rows: [] }; byKey.set(label, g); }
|
|
103
|
+
g.count++;
|
|
104
|
+
g.rows.push(r.values);
|
|
105
|
+
addSums(g.sums, r.values);
|
|
106
|
+
}
|
|
107
|
+
groups = Array.from(byKey.values()).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { title, generatedAt, columns: exportCols, groups, rows: flatRows, groupColumnId, sumColumnIds, totals };
|
|
111
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { CellValue } from '@ifc-lite/lists';
|
|
6
|
+
import { displayCell, type ExportModel } from './model';
|
|
7
|
+
|
|
8
|
+
/** Quality tabular PDF report: title + meta, dark header, grouped sections
|
|
9
|
+
* (bold group rows carrying per-group count + subtotals), right-aligned
|
|
10
|
+
* numerics, a grand-total foot, and page numbers. */
|
|
11
|
+
export async function toPdf(model: ExportModel): Promise<Blob> {
|
|
12
|
+
const { jsPDF } = await import('jspdf');
|
|
13
|
+
const autoTable = (await import('jspdf-autotable')).default;
|
|
14
|
+
|
|
15
|
+
const landscape = model.columns.length > 5;
|
|
16
|
+
const doc = new jsPDF({ orientation: landscape ? 'landscape' : 'portrait', unit: 'pt', format: 'a4' });
|
|
17
|
+
|
|
18
|
+
doc.setFont('helvetica', 'bold'); doc.setFontSize(14);
|
|
19
|
+
doc.text(model.title || 'List', 40, 42);
|
|
20
|
+
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(130);
|
|
21
|
+
doc.text(`${model.totals.count.toLocaleString()} elements · ${model.generatedAt}`, 40, 58);
|
|
22
|
+
doc.setTextColor(0);
|
|
23
|
+
|
|
24
|
+
const head = [model.columns.map((c) => c.label)];
|
|
25
|
+
const cell = (vals: CellValue[], i: number) => displayCell(vals[i]);
|
|
26
|
+
const body: Array<Array<string | { content: string; styles: Record<string, unknown> }>> = [];
|
|
27
|
+
|
|
28
|
+
if (model.groups) {
|
|
29
|
+
for (const g of model.groups) {
|
|
30
|
+
body.push(model.columns.map((c, i) => ({
|
|
31
|
+
content: i === 0 ? `${g.label} (${g.count})` : (c.summed ? displayCell(g.sums[c.id]) : ''),
|
|
32
|
+
styles: { fontStyle: 'bold', fillColor: [226, 232, 240] as unknown as number[] },
|
|
33
|
+
})));
|
|
34
|
+
for (const r of g.rows) body.push(model.columns.map((_, i) => cell(r, i)));
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
for (const r of model.rows) body.push(model.columns.map((_, i) => cell(r, i)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const foot = model.sumColumnIds.length > 0
|
|
41
|
+
? [model.columns.map((c, i) => (c.summed ? displayCell(model.totals.sums[c.id]) : (i === 0 ? `Total (${model.totals.count})` : '')))]
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const columnStyles: Record<number, { halign: 'right' }> = {};
|
|
45
|
+
model.columns.forEach((c, i) => { if (c.numeric) columnStyles[i] = { halign: 'right' }; });
|
|
46
|
+
|
|
47
|
+
autoTable(doc, {
|
|
48
|
+
head,
|
|
49
|
+
body,
|
|
50
|
+
foot,
|
|
51
|
+
startY: 72,
|
|
52
|
+
margin: { left: 40, right: 40, top: 70, bottom: 40 },
|
|
53
|
+
styles: { fontSize: 8, cellPadding: 3, overflow: 'ellipsize', lineColor: [226, 232, 240], lineWidth: 0.5 },
|
|
54
|
+
headStyles: { fillColor: [51, 65, 85], textColor: 255, fontStyle: 'bold' },
|
|
55
|
+
footStyles: { fillColor: [241, 245, 249], textColor: 20, fontStyle: 'bold' },
|
|
56
|
+
columnStyles,
|
|
57
|
+
didDrawPage: () => {
|
|
58
|
+
doc.setFontSize(8); doc.setTextColor(150);
|
|
59
|
+
const w = doc.internal.pageSize.getWidth();
|
|
60
|
+
const h = doc.internal.pageSize.getHeight();
|
|
61
|
+
doc.text(`Page ${doc.getNumberOfPages()}`, w - 60, h - 20);
|
|
62
|
+
doc.setTextColor(0);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return doc.output('blob');
|
|
67
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { CellValue } from '@ifc-lite/lists';
|
|
6
|
+
import { displayCell, type ExportModel, type ExportColumn } from './model';
|
|
7
|
+
|
|
8
|
+
const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
9
|
+
const NUM_FMT = '#,##0.####';
|
|
10
|
+
|
|
11
|
+
/** px → Excel column-width units (≈ 7px per char). */
|
|
12
|
+
const excelWidth = (px: number): number => Math.max(8, Math.min(80, Math.round(px / 7)));
|
|
13
|
+
|
|
14
|
+
/** Excel keeps real numbers (so the recipient can re-aggregate); other types
|
|
15
|
+
* fall back to the same display string the table shows. */
|
|
16
|
+
function cellValue(v: CellValue, c: ExportColumn): string | number | null {
|
|
17
|
+
if (v === null || v === undefined) return null;
|
|
18
|
+
if (c.numeric && typeof v === 'number' && Number.isFinite(v)) return v;
|
|
19
|
+
return displayCell(v);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function toXlsx(model: ExportModel): Promise<Blob> {
|
|
23
|
+
const ExcelJS = (await import('exceljs')).default;
|
|
24
|
+
const wb = new ExcelJS.Workbook();
|
|
25
|
+
wb.creator = 'IFC-Lite';
|
|
26
|
+
const ws = wb.addWorksheet((model.title || 'List').slice(0, 31), {
|
|
27
|
+
views: [{ state: 'frozen', ySplit: 4 }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const cols = model.columns;
|
|
31
|
+
const ncol = cols.length;
|
|
32
|
+
|
|
33
|
+
// Title + meta.
|
|
34
|
+
ws.addRow([model.title || 'List']);
|
|
35
|
+
ws.mergeCells(1, 1, 1, ncol);
|
|
36
|
+
ws.getCell(1, 1).font = { bold: true, size: 14 };
|
|
37
|
+
ws.addRow([`${model.totals.count.toLocaleString()} elements · ${model.generatedAt}`]);
|
|
38
|
+
ws.mergeCells(2, 1, 2, ncol);
|
|
39
|
+
ws.getCell(2, 1).font = { italic: true, size: 9, color: { argb: 'FF94A3B8' } };
|
|
40
|
+
ws.addRow([]);
|
|
41
|
+
|
|
42
|
+
// Header.
|
|
43
|
+
const header = ws.addRow(cols.map((c) => c.label));
|
|
44
|
+
header.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
|
45
|
+
header.eachCell((cell) => {
|
|
46
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF334155' } };
|
|
47
|
+
cell.alignment = { vertical: 'middle' };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Column widths + numeric formatting/alignment.
|
|
51
|
+
cols.forEach((c, i) => {
|
|
52
|
+
const col = ws.getColumn(i + 1);
|
|
53
|
+
col.width = excelWidth(c.width);
|
|
54
|
+
if (c.numeric) { col.numFmt = NUM_FMT; col.alignment = { horizontal: 'right' }; }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const addDataRow = (values: CellValue[], outline?: number) => {
|
|
58
|
+
const r = ws.addRow(cols.map((c, i) => cellValue(values[i], c)));
|
|
59
|
+
if (outline) r.outlineLevel = outline;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (model.groups) {
|
|
63
|
+
for (const g of model.groups) {
|
|
64
|
+
const gr = ws.addRow(cols.map((c, i) => (i === 0 ? `${g.label} (${g.count})` : (c.summed ? g.sums[c.id] : null))));
|
|
65
|
+
gr.font = { bold: true };
|
|
66
|
+
gr.eachCell((cell) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } }; });
|
|
67
|
+
for (const row of g.rows) addDataRow(row, 1);
|
|
68
|
+
}
|
|
69
|
+
ws.properties.outlineLevelRow = 1;
|
|
70
|
+
} else {
|
|
71
|
+
for (const row of model.rows) addDataRow(row);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Grand total.
|
|
75
|
+
if (model.sumColumnIds.length > 0) {
|
|
76
|
+
const tr = ws.addRow(cols.map((c, i) => (c.summed ? model.totals.sums[c.id] : (i === 0 ? `Total (${model.totals.count})` : null))));
|
|
77
|
+
tr.font = { bold: true };
|
|
78
|
+
tr.eachCell((cell) => { cell.border = { top: { style: 'double', color: { argb: 'FF334155' } } }; });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const buf = await wb.xlsx.writeBuffer();
|
|
82
|
+
return new Blob([buf], { type: XLSX_MIME });
|
|
83
|
+
}
|
package/src/lib/lists/index.ts
CHANGED
|
@@ -14,10 +14,12 @@ export type {
|
|
|
14
14
|
ConditionOperator,
|
|
15
15
|
DiscoveredColumns,
|
|
16
16
|
EntityAttribute,
|
|
17
|
+
ListGrouping,
|
|
17
18
|
} from '@ifc-lite/lists';
|
|
18
19
|
export {
|
|
19
20
|
ENTITY_ATTRIBUTES,
|
|
20
21
|
executeList,
|
|
22
|
+
summariseListRows,
|
|
21
23
|
listResultToCSV,
|
|
22
24
|
discoverColumns,
|
|
23
25
|
LIST_PRESETS,
|
|
@@ -456,6 +456,29 @@ export function applyScriptEditOperations(params: {
|
|
|
456
456
|
return { ok: true, content, selection, revision, appliedOpIds, changes, status: 'ok' };
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
if (operations.length > 1 && operations.some((op) => op.type === 'replaceAll')) {
|
|
460
|
+
const diagnostic = createPatchDiagnostic(
|
|
461
|
+
'patch_semantic_error',
|
|
462
|
+
'A replaceAll edit must be the only operation in its batch; it cannot be combined with positional ops.',
|
|
463
|
+
'error',
|
|
464
|
+
{
|
|
465
|
+
failureKind: 'mixed_repair_scopes',
|
|
466
|
+
fixHint:
|
|
467
|
+
'Emit replaceAll on its own, or use only positional ops (insert/replaceRange/append) in one batch.',
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
content: params.content,
|
|
473
|
+
selection: params.selection,
|
|
474
|
+
revision,
|
|
475
|
+
appliedOpIds: [],
|
|
476
|
+
status: 'semantic_error',
|
|
477
|
+
error: diagnostic.message,
|
|
478
|
+
diagnostic,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
459
482
|
if (params.intent === 'repair') {
|
|
460
483
|
const metadataError = validateRepairBatchMetadata(operations);
|
|
461
484
|
if (metadataError) {
|
|
@@ -139,7 +139,14 @@ export async function readSseStream(
|
|
|
139
139
|
if (!line.startsWith('data: ')) continue;
|
|
140
140
|
const data = line.slice(6);
|
|
141
141
|
if (data === '[DONE]') continue;
|
|
142
|
-
try {
|
|
142
|
+
try {
|
|
143
|
+
onEvent(data);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Malformed JSON payloads are expected and skipped, but a genuine
|
|
146
|
+
// callback failure (onChunk/onUsageInfo/logCacheHit/fullText) would
|
|
147
|
+
// otherwise be silently dropped — surface it for diagnosability.
|
|
148
|
+
console.debug('[sse] skipped event', err);
|
|
149
|
+
}
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
};
|