@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,325 @@
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 { buildErrorFeedbackContent } from './chatSlice.js';
8
+ import { create } from 'zustand';
9
+ import { createChatSlice, type ChatSlice } from './chatSlice.js';
10
+ import { createPatchDiagnostic, createPreflightDiagnostic } from '../../lib/llm/script-diagnostics.js';
11
+ import { DEFAULT_FREE_MODEL, DEFAULT_PRO_MODEL } from '../../lib/llm/models.js';
12
+
13
+ function withMockLocalStorage(fn: () => void) {
14
+ const original = globalThis.localStorage;
15
+ const store = new Map<string, string>();
16
+ Object.defineProperty(globalThis, 'localStorage', {
17
+ configurable: true,
18
+ value: {
19
+ getItem: (key: string) => store.get(key) ?? null,
20
+ setItem: (key: string, value: string) => { store.set(key, value); },
21
+ removeItem: (key: string) => { store.delete(key); },
22
+ },
23
+ });
24
+ try {
25
+ fn();
26
+ } finally {
27
+ Object.defineProperty(globalThis, 'localStorage', {
28
+ configurable: true,
29
+ value: original,
30
+ });
31
+ }
32
+ }
33
+
34
+ test('buildErrorFeedbackContent includes revision and diagnostics for patch conflicts', () => {
35
+ const prompt = buildErrorFeedbackContent(
36
+ 'const width = 30;',
37
+ 'Edit op "declare-width-depth" targets revision 3, but expected base revision is 4.',
38
+ {
39
+ currentRevision: 4,
40
+ reason: 'patch-conflict',
41
+ diagnostics: [
42
+ createPatchDiagnostic(
43
+ 'patch_revision_conflict',
44
+ 'Edit op "declare-width-depth" targets revision 3, but expected base revision is 4.',
45
+ 'error',
46
+ { attemptedOpIds: ['declare-width-depth'], opBaseRevision: 3, currentEditorRevision: 4 },
47
+ ),
48
+ ],
49
+ },
50
+ );
51
+
52
+ assert.match(prompt, /Failure type: patch-conflict/);
53
+ assert.match(prompt, /Current script revision: 4/);
54
+ assert.match(prompt, /Return exactly one `ifc-script-edits` block/);
55
+ assert.match(prompt, /Use exact SEARCH\/REPLACE blocks inside that fence/);
56
+ assert.match(prompt, /The script needs a root-cause repair\./);
57
+ assert.match(prompt, /\[patch:patch_revision_conflict\]/);
58
+ assert.match(prompt, /copy SEARCH blocks from that latest revision/);
59
+ });
60
+
61
+ test('buildErrorFeedbackContent includes structured preflight diagnostics', () => {
62
+ const prompt = buildErrorFeedbackContent(
63
+ 'bim.create.addIfcPlate(h, storey, { Width: 2, Height: 3, Thickness: 0.1 });',
64
+ 'Preflight validation failed.',
65
+ {
66
+ reason: 'preflight',
67
+ diagnostics: [
68
+ createPreflightDiagnostic(
69
+ 'create_contract',
70
+ '`bim.create.addIfcPlate(...)` uses `Depth` and `Thickness`, not `Height`.',
71
+ ),
72
+ ],
73
+ },
74
+ );
75
+
76
+ assert.match(prompt, /\[preflight:create_contract\]/);
77
+ assert.match(prompt, /current script that should be repaired in place/i);
78
+ });
79
+
80
+ test('buildErrorFeedbackContent surfaces grouped root cause evidence when diagnostics include ranges', () => {
81
+ const prompt = buildErrorFeedbackContent(
82
+ 'const script = true;',
83
+ 'Preflight validation failed.',
84
+ {
85
+ reason: 'preflight',
86
+ diagnostics: [
87
+ createPreflightDiagnostic(
88
+ 'wall_hosted_opening_pattern',
89
+ 'Suspicious pattern: `bim.create.addIfcDoor(...)`...',
90
+ 'error',
91
+ {
92
+ methodName: 'addIfcDoor',
93
+ range: { from: 120, to: 180 },
94
+ snippet: 'bim.create.addIfcDoor(h, ground, { Name: "Front Door" });',
95
+ },
96
+ ),
97
+ ],
98
+ },
99
+ );
100
+
101
+ assert.match(prompt, /Root cause to fix first:/);
102
+ assert.match(prompt, /key: placement_context_mismatch/);
103
+ assert.match(prompt, /scope: block/);
104
+ assert.match(prompt, /Supporting evidence:/);
105
+ assert.match(prompt, /method=addIfcDoor/);
106
+ assert.match(prompt, /range=120\.\.180/);
107
+ assert.match(prompt, /Front Door/);
108
+ });
109
+
110
+ test('buildErrorFeedbackContent reinforces preservation rules for patch apply failures', () => {
111
+ const prompt = buildErrorFeedbackContent(
112
+ 'const h = bim.create.project({ Name: "Tower" });',
113
+ 'Repair turns cannot use `replaceAll` unless the system explicitly allows a full rewrite.',
114
+ {
115
+ reason: 'patch-apply',
116
+ currentRevision: 9,
117
+ diagnostics: [
118
+ createPatchDiagnostic(
119
+ 'unsafe_full_replacement',
120
+ 'Repair turns cannot use `replaceAll` unless the system explicitly allows a full rewrite.',
121
+ ),
122
+ ],
123
+ },
124
+ );
125
+
126
+ assert.match(prompt, /Failure type: patch-apply/);
127
+ assert.match(prompt, /Do NOT use `replaceAll` unless the user explicitly asked to regenerate the full script/);
128
+ assert.match(prompt, /Use exact SEARCH\/REPLACE blocks inside that fence/);
129
+ assert.match(prompt, /Copy SEARCH text verbatim from the CURRENT script/);
130
+ assert.match(prompt, /keep the full script intact and patch only the necessary regions/);
131
+ assert.doesNotMatch(prompt, /Provide a corrected version/);
132
+ });
133
+
134
+ test('buildErrorFeedbackContent can include live selection and stale code block context', () => {
135
+ const prompt = buildErrorFeedbackContent(
136
+ 'const live = true;',
137
+ 'ReferenceError: width is not defined',
138
+ {
139
+ currentRevision: 5,
140
+ currentSelection: { from: 6, to: 10 },
141
+ staleCodeBlock: 'const width = 30;',
142
+ diagnostics: [
143
+ createPatchDiagnostic(
144
+ 'patch_semantic_error',
145
+ 'Malformed repair reply mixed a js fence with edit ops.',
146
+ 'error',
147
+ { failureKind: 'parse_error', fixHint: 'Return one patch block only.' },
148
+ ),
149
+ ],
150
+ },
151
+ );
152
+
153
+ assert.match(prompt, /Current selection: from=6, to=10/);
154
+ assert.match(prompt, /it may be stale relative to the editor/);
155
+ assert.match(prompt, /failure=parse_error/);
156
+ assert.match(prompt, /Hint: Return one patch block only\./);
157
+ });
158
+
159
+ test('buildErrorFeedbackContent groups multiple local diagnostics under one root cause', () => {
160
+ const prompt = buildErrorFeedbackContent(
161
+ 'const h = bim.create.project({ Name: "House" });',
162
+ 'Preflight validation failed.',
163
+ {
164
+ reason: 'preflight',
165
+ diagnostics: [
166
+ createPreflightDiagnostic(
167
+ 'wall_hosted_opening_pattern',
168
+ 'Suspicious door call.',
169
+ 'error',
170
+ {
171
+ methodName: 'addIfcDoor',
172
+ range: { from: 10, to: 20 },
173
+ snippet: 'bim.create.addIfcDoor(...)',
174
+ },
175
+ ),
176
+ createPreflightDiagnostic(
177
+ 'world_placement_elevation',
178
+ 'Suspicious repeated world placement.',
179
+ 'error',
180
+ {
181
+ methodName: 'addIfcMember',
182
+ range: { from: 30, to: 40 },
183
+ snippet: 'bim.create.addIfcMember(...)',
184
+ },
185
+ ),
186
+ ],
187
+ },
188
+ );
189
+
190
+ assert.equal((prompt.match(/\[root-cause:placement_context_mismatch\]/g) ?? []).length, 1);
191
+ assert.match(prompt, /scope=block/);
192
+ assert.match(prompt, /supporting evidence: preflight:wall_hosted_opening_pattern/);
193
+ assert.match(prompt, /supporting evidence: preflight:world_placement_elevation/);
194
+ });
195
+
196
+ test('clearChatMessages resets streaming state as well as persisted messages', () => {
197
+ const useChatStore = create<ChatSlice>()((...args) => createChatSlice(...args));
198
+ const abortController = new AbortController();
199
+
200
+ useChatStore.getState().addChatMessage({
201
+ id: '1',
202
+ role: 'user',
203
+ content: 'Create a house',
204
+ createdAt: Date.now(),
205
+ });
206
+ useChatStore.getState().setChatStatus('streaming');
207
+ useChatStore.getState().setChatStreamingContent('partial');
208
+ useChatStore.getState().setChatAbortController(abortController);
209
+
210
+ useChatStore.getState().clearChatMessages();
211
+
212
+ assert.deepEqual(useChatStore.getState().chatMessages, []);
213
+ assert.equal(useChatStore.getState().chatStatus, 'idle');
214
+ assert.equal(useChatStore.getState().chatStreamingContent, '');
215
+ assert.equal(useChatStore.getState().chatAbortController, null);
216
+ assert.equal(abortController.signal.aborted, true);
217
+ assert.deepEqual(useChatStore.getState().chatAttachments, []);
218
+ });
219
+
220
+ test('switchChatUserContext restores per-user history and coerces disallowed models', () => {
221
+ withMockLocalStorage(() => {
222
+ globalThis.localStorage.setItem('ifc-lite-chat-model:user-a', DEFAULT_PRO_MODEL.id);
223
+ globalThis.localStorage.setItem('ifc-lite-chat-messages:user-a', JSON.stringify([
224
+ {
225
+ id: 'persisted-a',
226
+ role: 'user',
227
+ content: 'hello from A',
228
+ createdAt: 1,
229
+ },
230
+ ]));
231
+ globalThis.localStorage.setItem('ifc-lite-chat-model:user-b', DEFAULT_FREE_MODEL.id);
232
+ globalThis.localStorage.setItem('ifc-lite-chat-messages:user-b', JSON.stringify([
233
+ {
234
+ id: 'persisted-b',
235
+ role: 'assistant',
236
+ content: 'hello from B',
237
+ createdAt: 2,
238
+ },
239
+ ]));
240
+
241
+ const useChatStore = create<ChatSlice>()((...args) => createChatSlice(...args));
242
+ useChatStore.getState().switchChatUserContext('user-a', true, { restoreMessages: true });
243
+
244
+ assert.equal(useChatStore.getState().chatActiveModel, DEFAULT_PRO_MODEL.id);
245
+ assert.equal(useChatStore.getState().chatMessages[0]?.id, 'persisted-a');
246
+
247
+ useChatStore.getState().switchChatUserContext('user-b', false, {
248
+ clearPersistedCurrent: true,
249
+ restoreMessages: true,
250
+ });
251
+
252
+ assert.equal(useChatStore.getState().chatActiveModel, DEFAULT_FREE_MODEL.id);
253
+ assert.equal(useChatStore.getState().chatMessages[0]?.id, 'persisted-b');
254
+ assert.equal(globalThis.localStorage.getItem('ifc-lite-chat-messages:user-a'), null);
255
+ });
256
+ });
257
+
258
+ test('setChatHasPro falls back to a free model when entitlement is removed', () => {
259
+ const useChatStore = create<ChatSlice>()((...args) => createChatSlice(...args));
260
+ useChatStore.getState().setChatHasPro(true);
261
+ useChatStore.getState().setChatActiveModel(DEFAULT_PRO_MODEL.id);
262
+
263
+ useChatStore.getState().setChatHasPro(false);
264
+
265
+ assert.equal(useChatStore.getState().chatHasPro, false);
266
+ assert.equal(useChatStore.getState().chatActiveModel, DEFAULT_FREE_MODEL.id);
267
+ });
268
+
269
+ test('removeChatAttachment only removes the targeted attachment id', () => {
270
+ const useChatStore = create<ChatSlice>()((...args) => createChatSlice(...args));
271
+ useChatStore.getState().addChatAttachment({
272
+ id: 'a-1',
273
+ name: 'duplicate.csv',
274
+ type: 'text/csv',
275
+ size: 12,
276
+ textContent: 'a,b\n1,2',
277
+ });
278
+ useChatStore.getState().addChatAttachment({
279
+ id: 'a-2',
280
+ name: 'duplicate.csv',
281
+ type: 'text/csv',
282
+ size: 12,
283
+ textContent: 'a,b\n3,4',
284
+ });
285
+
286
+ useChatStore.getState().removeChatAttachment('a-1');
287
+
288
+ assert.deepEqual(
289
+ useChatStore.getState().chatAttachments.map((attachment) => attachment.id),
290
+ ['a-2'],
291
+ );
292
+ });
293
+
294
+ test('switchChatUserContext ignores malformed persisted messages', () => {
295
+ withMockLocalStorage(() => {
296
+ globalThis.localStorage.setItem('ifc-lite-chat-messages:user-a', JSON.stringify([
297
+ {
298
+ id: 'valid',
299
+ role: 'user',
300
+ content: 'hello',
301
+ createdAt: 1,
302
+ attachments: [
303
+ { id: 'att-1', name: 'ok.csv', type: 'text/csv', size: 20, textContent: 'a,b\n1,2' },
304
+ { name: 'missing-id.csv', type: 'text/csv', size: 20 },
305
+ ],
306
+ },
307
+ {
308
+ id: 123,
309
+ role: 'assistant',
310
+ content: 'bad',
311
+ createdAt: 2,
312
+ },
313
+ ]));
314
+
315
+ const useChatStore = create<ChatSlice>()((...args) => createChatSlice(...args));
316
+ useChatStore.getState().switchChatUserContext('user-a', false, { restoreMessages: true });
317
+
318
+ assert.equal(useChatStore.getState().chatMessages.length, 1);
319
+ assert.equal(useChatStore.getState().chatMessages[0]?.id, 'valid');
320
+ assert.deepEqual(
321
+ useChatStore.getState().chatMessages[0]?.attachments?.map((attachment) => attachment.id),
322
+ ['att-1'],
323
+ );
324
+ });
325
+ });