@ifc-lite/viewer 1.14.2 → 1.14.4

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
  3. package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
  4. package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
  5. package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
  6. package/dist/assets/index-CMQ_Dgkr.css +1 -0
  7. package/dist/assets/index-D7nEDctQ.js +229 -0
  8. package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
  9. package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
  10. package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
  11. package/dist/index.html +2 -2
  12. package/package.json +21 -20
  13. package/src/App.tsx +17 -1
  14. package/src/components/viewer/BasketPresentationDock.tsx +8 -4
  15. package/src/components/viewer/ChatPanel.tsx +1402 -0
  16. package/src/components/viewer/CodeEditor.tsx +70 -4
  17. package/src/components/viewer/CommandPalette.tsx +1 -0
  18. package/src/components/viewer/HierarchyPanel.tsx +28 -13
  19. package/src/components/viewer/MainToolbar.tsx +113 -95
  20. package/src/components/viewer/ScriptPanel.tsx +351 -184
  21. package/src/components/viewer/UpgradePage.tsx +69 -0
  22. package/src/components/viewer/Viewport.tsx +23 -0
  23. package/src/components/viewer/chat/ChatMessage.tsx +144 -0
  24. package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
  25. package/src/components/viewer/chat/ModelSelector.tsx +102 -0
  26. package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
  27. package/src/components/viewer/chat/renderTextContent.ts +19 -0
  28. package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
  29. package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
  30. package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
  31. package/src/components/viewer/hierarchy/types.ts +6 -1
  32. package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
  33. package/src/hooks/useIfcCache.ts +1 -2
  34. package/src/hooks/useSandbox.ts +122 -6
  35. package/src/index.css +10 -0
  36. package/src/lib/attachments.ts +46 -0
  37. package/src/lib/llm/ClerkChatSync.tsx +74 -0
  38. package/src/lib/llm/clerk-auth.ts +62 -0
  39. package/src/lib/llm/code-extractor.ts +50 -0
  40. package/src/lib/llm/context-builder.test.ts +18 -0
  41. package/src/lib/llm/context-builder.ts +305 -0
  42. package/src/lib/llm/free-models.test.ts +118 -0
  43. package/src/lib/llm/message-capabilities.test.ts +131 -0
  44. package/src/lib/llm/message-capabilities.ts +94 -0
  45. package/src/lib/llm/models.ts +197 -0
  46. package/src/lib/llm/repair-loop.test.ts +91 -0
  47. package/src/lib/llm/repair-loop.ts +76 -0
  48. package/src/lib/llm/script-diagnostics.ts +445 -0
  49. package/src/lib/llm/script-edit-ops.test.ts +399 -0
  50. package/src/lib/llm/script-edit-ops.ts +954 -0
  51. package/src/lib/llm/script-preflight.test.ts +513 -0
  52. package/src/lib/llm/script-preflight.ts +990 -0
  53. package/src/lib/llm/script-preservation.test.ts +128 -0
  54. package/src/lib/llm/script-preservation.ts +152 -0
  55. package/src/lib/llm/stream-client.test.ts +97 -0
  56. package/src/lib/llm/stream-client.ts +410 -0
  57. package/src/lib/llm/system-prompt.test.ts +181 -0
  58. package/src/lib/llm/system-prompt.ts +665 -0
  59. package/src/lib/llm/types.ts +150 -0
  60. package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
  61. package/src/lib/scripts/templates/create-building.ts +12 -12
  62. package/src/main.tsx +10 -1
  63. package/src/sdk/adapters/export-adapter.test.ts +24 -0
  64. package/src/sdk/adapters/export-adapter.ts +40 -16
  65. package/src/sdk/adapters/files-adapter.ts +39 -0
  66. package/src/sdk/adapters/model-compat.ts +1 -1
  67. package/src/sdk/adapters/mutate-adapter.ts +20 -6
  68. package/src/sdk/adapters/mutation-view.ts +112 -0
  69. package/src/sdk/adapters/query-adapter.ts +100 -4
  70. package/src/sdk/local-backend.ts +4 -0
  71. package/src/store/index.ts +15 -1
  72. package/src/store/slices/chatSlice.test.ts +325 -0
  73. package/src/store/slices/chatSlice.ts +468 -0
  74. package/src/store/slices/scriptSlice.test.ts +75 -0
  75. package/src/store/slices/scriptSlice.ts +256 -9
  76. package/src/vite-env.d.ts +10 -0
  77. package/vite.config.ts +21 -2
  78. package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
  79. package/dist/assets/index-ByrFvN5A.css +0 -1
  80. package/dist/assets/index-CN7qDq7G.js +0 -216
@@ -0,0 +1,399 @@
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 test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { applyScriptEditOperations, extractScriptEditOps, filterUnappliedScriptOps } from './script-edit-ops.js';
8
+
9
+ test('extractScriptEditOps parses ops from ifc-script-edits block', () => {
10
+ const text = `
11
+ Hello
12
+ \`\`\`ifc-script-edits
13
+ {"scriptEdits":[
14
+ {"opId":"op-1","type":"replaceSelection","baseRevision":3,"text":"const x = 1;"},
15
+ {"opId":"op-2","type":"append","baseRevision":3,"text":"\\nconsole.log(x)"}
16
+ ]}
17
+ \`\`\`
18
+ `;
19
+ const result = extractScriptEditOps(text);
20
+ assert.equal(result.parseErrors.length, 0);
21
+ assert.equal(result.operations.length, 2);
22
+ assert.equal(result.operations[0].opId, 'op-1');
23
+ assert.equal(result.operations[1].type, 'append');
24
+ });
25
+
26
+ test('extractScriptEditOps parses SEARCH/REPLACE blocks into replaceRange ops', () => {
27
+ const baseContent = 'const width = 30;\nconst depth = 40;\n';
28
+ const result = extractScriptEditOps(`
29
+ \`\`\`ifc-script-edits
30
+ <<<<<<< SEARCH
31
+ const width = 30;
32
+ =======
33
+ const width = 36;
34
+ >>>>>>> REPLACE
35
+ \`\`\`
36
+ `, {
37
+ baseRevision: 7,
38
+ baseContent,
39
+ intent: 'repair',
40
+ requestedRepairScope: 'local',
41
+ targetRootCause: 'api_contract_mismatch',
42
+ });
43
+
44
+ assert.equal(result.parseErrors.length, 0);
45
+ assert.equal(result.operations.length, 1);
46
+ assert.equal(result.operations[0].type, 'replaceRange');
47
+ assert.equal(result.operations[0].baseRevision, 7);
48
+ assert.equal(result.operations[0].expectedText, 'const width = 30;');
49
+ assert.equal(result.operations[0].text, 'const width = 36;');
50
+ });
51
+
52
+ test('extractScriptEditOps reports no-match SEARCH blocks with targeted diagnostics', () => {
53
+ const result = extractScriptEditOps(`
54
+ \`\`\`ifc-script-edits
55
+ <<<<<<< SEARCH
56
+ const missing = true;
57
+ =======
58
+ const present = true;
59
+ >>>>>>> REPLACE
60
+ \`\`\`
61
+ `, {
62
+ baseRevision: 4,
63
+ baseContent: 'const width = 30;\n',
64
+ intent: 'repair',
65
+ });
66
+
67
+ assert.equal(result.operations.length, 0);
68
+ assert.match(result.parseErrors[0] ?? '', /does not match the current script/);
69
+ assert.equal(result.parseDiagnostics[0]?.data?.failureKind, 'no_unique_match');
70
+ });
71
+
72
+ test('extractScriptEditOps reports ambiguous SEARCH blocks with targeted diagnostics', () => {
73
+ const result = extractScriptEditOps(`
74
+ \`\`\`ifc-script-edits
75
+ <<<<<<< SEARCH
76
+ const value = 1;
77
+ =======
78
+ const value = 2;
79
+ >>>>>>> REPLACE
80
+ \`\`\`
81
+ `, {
82
+ baseRevision: 4,
83
+ baseContent: 'const value = 1;\nconst value = 1;\n',
84
+ intent: 'repair',
85
+ });
86
+
87
+ assert.equal(result.operations.length, 0);
88
+ assert.match(result.parseErrors[0] ?? '', /matches multiple locations/);
89
+ assert.equal(result.parseDiagnostics[0]?.data?.failureKind, 'multiple_matches');
90
+ });
91
+
92
+ test('filterUnappliedScriptOps drops already applied op ids', () => {
93
+ const result = extractScriptEditOps(`
94
+ \`\`\`ifc-script-edits
95
+ {"scriptEdits":[
96
+ {"opId":"op-1","type":"append","baseRevision":1,"text":"a"},
97
+ {"opId":"op-2","type":"append","baseRevision":1,"text":"b"}
98
+ ]}
99
+ \`\`\`
100
+ `);
101
+ const filtered = filterUnappliedScriptOps(result.operations, new Set(['op-1']));
102
+ assert.equal(filtered.length, 1);
103
+ assert.equal(filtered[0].opId, 'op-2');
104
+ });
105
+
106
+ test('applyScriptEditOperations applies replaceSelection and append in one revision', () => {
107
+ const applied = applyScriptEditOperations({
108
+ content: 'const a = 1;',
109
+ selection: { from: 6, to: 7 },
110
+ revision: 5,
111
+ operations: [
112
+ { opId: 'op-1', type: 'replaceSelection', baseRevision: 5, text: 'b' },
113
+ { opId: 'op-2', type: 'append', baseRevision: 5, text: '\nconsole.log(b)' },
114
+ ],
115
+ });
116
+ assert.equal(applied.ok, true);
117
+ assert.equal(applied.status, 'ok');
118
+ assert.equal(applied.revision, 6);
119
+ assert.equal(applied.content, 'const b = 1;\nconsole.log(b)');
120
+ assert.deepEqual(applied.appliedOpIds, ['op-1', 'op-2']);
121
+ });
122
+
123
+ test('applyScriptEditOperations rejects stale revision', () => {
124
+ const applied = applyScriptEditOperations({
125
+ content: 'const a = 1;',
126
+ selection: { from: 0, to: 0 },
127
+ revision: 2,
128
+ operations: [
129
+ { opId: 'op-1', type: 'append', baseRevision: 1, text: '\nconsole.log(a)' },
130
+ ],
131
+ });
132
+ assert.equal(applied.ok, false);
133
+ assert.equal(applied.status, 'revision_conflict');
134
+ assert.match(applied.error ?? '', /expected base revision is 2/);
135
+ assert.equal(applied.diagnostic?.code, 'patch_revision_conflict');
136
+ });
137
+
138
+ test('applyScriptEditOperations accepts fixed base revision across stream batches', () => {
139
+ const baseContent = 'if (f > 0) {\n facade();\n}';
140
+ const first = applyScriptEditOperations({
141
+ content: baseContent,
142
+ selection: { from: 0, to: 0 },
143
+ revision: 1,
144
+ acceptedBaseRevision: 1,
145
+ baseContentSnapshot: baseContent,
146
+ operations: [
147
+ {
148
+ opId: 'remove-ground-skip',
149
+ type: 'replaceRange',
150
+ baseRevision: 1,
151
+ from: 0,
152
+ to: 11,
153
+ text: '',
154
+ },
155
+ ],
156
+ });
157
+ assert.equal(first.ok, true);
158
+ assert.equal(first.status, 'ok');
159
+ assert.equal(first.revision, 2);
160
+
161
+ const second = applyScriptEditOperations({
162
+ content: first.content,
163
+ selection: first.selection,
164
+ revision: first.revision,
165
+ priorAcceptedOps: [
166
+ { opId: 'prefix', type: 'insert', baseRevision: 1, at: 0, text: '12' },
167
+ ],
168
+ acceptedBaseRevision: 1,
169
+ baseContentSnapshot: baseContent,
170
+ operations: [
171
+ {
172
+ opId: 'normalize-indent',
173
+ type: 'replaceRange',
174
+ baseRevision: 1,
175
+ from: 0,
176
+ to: 0,
177
+ text: '// edited\n',
178
+ },
179
+ ],
180
+ });
181
+ assert.equal(second.ok, true);
182
+ assert.equal(second.status, 'ok');
183
+ assert.equal(second.revision, 3);
184
+ });
185
+
186
+ test('applyScriptEditOperations rebases positional ops against the original base snapshot', () => {
187
+ const baseContent = 'abcdef';
188
+ const first = applyScriptEditOperations({
189
+ content: baseContent,
190
+ selection: { from: 0, to: 0 },
191
+ revision: 1,
192
+ acceptedBaseRevision: 1,
193
+ baseContentSnapshot: baseContent,
194
+ operations: [
195
+ { opId: 'prefix', type: 'insert', baseRevision: 1, at: 0, text: '12' },
196
+ ],
197
+ });
198
+ assert.equal(first.ok, true);
199
+ assert.equal(first.content, '12abcdef');
200
+
201
+ const second = applyScriptEditOperations({
202
+ content: first.content,
203
+ selection: first.selection,
204
+ revision: first.revision,
205
+ priorAcceptedOps: [
206
+ { opId: 'prefix', type: 'insert', baseRevision: 1, at: 0, text: '12' },
207
+ ],
208
+ acceptedBaseRevision: 1,
209
+ baseContentSnapshot: baseContent,
210
+ operations: [
211
+ { opId: 'replace-cd', type: 'replaceRange', baseRevision: 1, from: 2, to: 4, text: 'ZZ' },
212
+ ],
213
+ });
214
+ assert.equal(second.ok, true);
215
+ assert.equal(second.content, '12abZZef');
216
+ });
217
+
218
+ test('applyScriptEditOperations rejects overlapping stale range ops before mutating content', () => {
219
+ const baseContent = 'const width = 30;\nconst depth = 40;\n';
220
+ const applied = applyScriptEditOperations({
221
+ content: baseContent,
222
+ selection: { from: 0, to: 0 },
223
+ revision: 3,
224
+ acceptedBaseRevision: 3,
225
+ baseContentSnapshot: baseContent,
226
+ operations: [
227
+ { opId: 'rename-width', type: 'replaceRange', baseRevision: 3, from: 6, to: 11, text: 'span' },
228
+ { opId: 'rename-width-again', type: 'replaceRange', baseRevision: 3, from: 8, to: 13, text: 'measure' },
229
+ ],
230
+ });
231
+
232
+ assert.equal(applied.ok, false);
233
+ assert.equal(applied.status, 'revision_conflict');
234
+ assert.equal(applied.content, baseContent);
235
+ assert.equal(applied.diagnostic?.code, 'patch_revision_conflict');
236
+ });
237
+
238
+ test('applyScriptEditOperations blocks replaceAll during repair turns', () => {
239
+ const applied = applyScriptEditOperations({
240
+ content: 'const h = bim.create.project({ Name: "Tower" });\nconst result = bim.create.toIfc(h);\n',
241
+ selection: { from: 0, to: 0 },
242
+ revision: 4,
243
+ intent: 'repair',
244
+ operations: [
245
+ {
246
+ opId: 'rewrite-fragment',
247
+ type: 'replaceAll',
248
+ baseRevision: 4,
249
+ text: 'for (let x = 0; x < width; x += 3) {\n facade(x);\n}\n',
250
+ },
251
+ ],
252
+ });
253
+
254
+ assert.equal(applied.ok, false);
255
+ assert.equal(applied.status, 'semantic_error');
256
+ assert.equal(applied.diagnostic?.code, 'unsafe_full_replacement');
257
+ });
258
+
259
+ test('applyScriptEditOperations rejects replaceSelection during repair turns', () => {
260
+ const applied = applyScriptEditOperations({
261
+ content: 'bim.create.addIfcDoor(h, ground, { Name: "Front Door" });',
262
+ selection: { from: 0, to: 10 },
263
+ revision: 6,
264
+ intent: 'repair',
265
+ operations: [
266
+ {
267
+ opId: 'repair-via-selection',
268
+ type: 'replaceSelection',
269
+ baseRevision: 6,
270
+ text: '',
271
+ },
272
+ ],
273
+ });
274
+
275
+ assert.equal(applied.ok, false);
276
+ assert.equal(applied.status, 'semantic_error');
277
+ assert.match(applied.error ?? '', /not allowed for automated repair turns/);
278
+ });
279
+
280
+ test('applyScriptEditOperations rejects repair range ops when expectedText does not match', () => {
281
+ const content = 'bim.create.addIfcDoor(h, ground, { Name: "Front Door" });';
282
+ const applied = applyScriptEditOperations({
283
+ content,
284
+ selection: { from: 0, to: 0 },
285
+ revision: 8,
286
+ intent: 'repair',
287
+ acceptedBaseRevision: 8,
288
+ baseContentSnapshot: content,
289
+ operations: [
290
+ {
291
+ opId: 'remove-door',
292
+ type: 'replaceRange',
293
+ baseRevision: 8,
294
+ from: 0,
295
+ to: content.length,
296
+ expectedText: 'bim.create.addIfcDoor(h, ground, { Name: "Wrong" });',
297
+ text: '// removed',
298
+ },
299
+ ],
300
+ });
301
+
302
+ assert.equal(applied.ok, false);
303
+ assert.equal(applied.status, 'revision_conflict');
304
+ assert.match(applied.error ?? '', /no longer matches the expected text/);
305
+ });
306
+
307
+ test('applyScriptEditOperations accepts coordinated structural repair ops with shared metadata', () => {
308
+ const content = [
309
+ 'const width = 30;',
310
+ 'const depth = 40;',
311
+ 'bim.create.addIfcMember(h, storey, {',
312
+ ' Start: [0, 0, 0],',
313
+ ' End: [0, 0, 3],',
314
+ ' Width: 0.2,',
315
+ ' Height: 0.2,',
316
+ '});',
317
+ ].join('\n');
318
+ const startLine = ' Start: [0, 0, 0],';
319
+ const endLine = ' End: [0, 0, 3],';
320
+ const startFrom = content.indexOf(startLine);
321
+ const endFrom = content.indexOf(endLine);
322
+
323
+ const applied = applyScriptEditOperations({
324
+ content,
325
+ selection: { from: 0, to: 0 },
326
+ revision: 10,
327
+ intent: 'repair',
328
+ acceptedBaseRevision: 10,
329
+ baseContentSnapshot: content,
330
+ operations: [
331
+ {
332
+ opId: 'fix-start',
333
+ type: 'replaceRange',
334
+ baseRevision: 10,
335
+ groupId: 'placement-fix',
336
+ scope: 'structural',
337
+ targetRootCause: 'placement_context_mismatch',
338
+ from: startFrom,
339
+ to: startFrom + startLine.length,
340
+ expectedText: startLine,
341
+ text: ' Start: [0, 0, elevation],',
342
+ },
343
+ {
344
+ opId: 'fix-end',
345
+ type: 'replaceRange',
346
+ baseRevision: 10,
347
+ groupId: 'placement-fix',
348
+ scope: 'structural',
349
+ targetRootCause: 'placement_context_mismatch',
350
+ from: endFrom,
351
+ to: endFrom + endLine.length,
352
+ expectedText: endLine,
353
+ text: ' End: [0, 0, elevation + 3],',
354
+ },
355
+ ],
356
+ });
357
+
358
+ assert.equal(applied.ok, true);
359
+ assert.match(applied.content, /Start: \[0, 0, elevation\]/);
360
+ assert.match(applied.content, /End: \[0, 0, elevation \+ 3\]/);
361
+ });
362
+
363
+ test('applyScriptEditOperations rejects grouped structural repair ops without shared metadata', () => {
364
+ const content = 'const width = 30;\nconst depth = 40;\n';
365
+ const applied = applyScriptEditOperations({
366
+ content,
367
+ selection: { from: 0, to: 0 },
368
+ revision: 11,
369
+ intent: 'repair',
370
+ acceptedBaseRevision: 11,
371
+ baseContentSnapshot: content,
372
+ operations: [
373
+ {
374
+ opId: 'fix-width',
375
+ type: 'replaceRange',
376
+ baseRevision: 11,
377
+ scope: 'structural',
378
+ from: 0,
379
+ to: 5,
380
+ expectedText: 'const',
381
+ text: 'let',
382
+ },
383
+ {
384
+ opId: 'fix-depth',
385
+ type: 'replaceRange',
386
+ baseRevision: 11,
387
+ scope: 'structural',
388
+ from: 17,
389
+ to: 22,
390
+ expectedText: 'const',
391
+ text: 'let',
392
+ },
393
+ ],
394
+ });
395
+
396
+ assert.equal(applied.ok, false);
397
+ assert.equal(applied.status, 'semantic_error');
398
+ assert.match(applied.error ?? '', /must declare `targetRootCause`/);
399
+ });