@ifc-lite/viewer 1.17.4 → 1.18.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 +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -8,7 +8,7 @@ import { buildErrorFeedbackContent } from './chatSlice.js';
|
|
|
8
8
|
import { create } from 'zustand';
|
|
9
9
|
import { createChatSlice, type ChatSlice } from './chatSlice.js';
|
|
10
10
|
import { createPatchDiagnostic, createPreflightDiagnostic } from '../../lib/llm/script-diagnostics.js';
|
|
11
|
-
import { DEFAULT_FREE_MODEL,
|
|
11
|
+
import { DEFAULT_FREE_MODEL, DEFAULT_BYOK_MODEL } from '../../lib/llm/models.js';
|
|
12
12
|
|
|
13
13
|
function withMockLocalStorage(fn: () => void) {
|
|
14
14
|
const original = globalThis.localStorage;
|
|
@@ -217,52 +217,14 @@ test('clearChatMessages resets streaming state as well as persisted messages', (
|
|
|
217
217
|
assert.deepEqual(useChatStore.getState().chatAttachments, []);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
test('
|
|
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', () => {
|
|
220
|
+
test('setChatHasByokKey falls back to a free model when keys are removed', () => {
|
|
259
221
|
const useChatStore = create<ChatSlice>()((...args) => createChatSlice(...args));
|
|
260
|
-
useChatStore.getState().
|
|
261
|
-
useChatStore.getState().setChatActiveModel(
|
|
222
|
+
useChatStore.getState().setChatHasByokKey(true);
|
|
223
|
+
useChatStore.getState().setChatActiveModel(DEFAULT_BYOK_MODEL.id);
|
|
262
224
|
|
|
263
|
-
useChatStore.getState().
|
|
225
|
+
useChatStore.getState().setChatHasByokKey(false);
|
|
264
226
|
|
|
265
|
-
assert.equal(useChatStore.getState().
|
|
227
|
+
assert.equal(useChatStore.getState().chatHasByokKey, false);
|
|
266
228
|
assert.equal(useChatStore.getState().chatActiveModel, DEFAULT_FREE_MODEL.id);
|
|
267
229
|
});
|
|
268
230
|
|
|
@@ -291,35 +253,3 @@ test('removeChatAttachment only removes the targeted attachment id', () => {
|
|
|
291
253
|
);
|
|
292
254
|
});
|
|
293
255
|
|
|
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
|
-
});
|
|
@@ -13,6 +13,7 @@ import { coerceModelForEntitlement, DEFAULT_FREE_MODEL } from '../../lib/llm/mod
|
|
|
13
13
|
import { extractCodeBlocks } from '../../lib/llm/code-extractor.js';
|
|
14
14
|
import type { ScriptDiagnostic } from '../../lib/llm/script-diagnostics.js';
|
|
15
15
|
import { formatDiagnosticsForPrompt, getPrimaryRootCause, groupDiagnosticsByRootCause } from '../../lib/llm/script-diagnostics.js';
|
|
16
|
+
import { hasAnyApiKey as hasAnyApiKeyAtInit } from '../../services/api-keys.js';
|
|
16
17
|
|
|
17
18
|
const MODEL_STORAGE_KEY = 'ifc-lite-chat-model';
|
|
18
19
|
const MESSAGES_STORAGE_KEY = 'ifc-lite-chat-messages';
|
|
@@ -43,11 +44,9 @@ export interface ChatSlice {
|
|
|
43
44
|
chatPendingRepairRequest: ChatRepairRequest | null;
|
|
44
45
|
/** Auto-captured viewport screenshot (base64 data URL) to include with next LLM message */
|
|
45
46
|
chatViewportScreenshot: string | null;
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
chatHasPro: boolean;
|
|
50
|
-
/** Usage info from the server: credits (pro) or request count (free) */
|
|
47
|
+
/** Whether the user has at least one BYOK API key configured */
|
|
48
|
+
chatHasByokKey: boolean;
|
|
49
|
+
/** Usage info from the proxy: request count for free tier */
|
|
51
50
|
chatUsage: ChatUsage | null;
|
|
52
51
|
/** User ID used to scope persisted model preference (null for anonymous). */
|
|
53
52
|
chatStorageUserId: string | null;
|
|
@@ -78,18 +77,10 @@ export interface ChatSlice {
|
|
|
78
77
|
sendErrorFeedback: (code: string, error: string) => void;
|
|
79
78
|
/** Store a viewport screenshot to include with the next LLM message */
|
|
80
79
|
setChatViewportScreenshot: (dataUrl: string | null) => void;
|
|
81
|
-
/** Set
|
|
82
|
-
|
|
83
|
-
/** Set whether user has pro subscription (called by ClerkProvider wrapper) */
|
|
84
|
-
setChatHasPro: (hasPro: boolean) => void;
|
|
80
|
+
/** Set whether user has at least one BYOK API key configured */
|
|
81
|
+
setChatHasByokKey: (hasByokKey: boolean) => void;
|
|
85
82
|
/** Update usage info from server response headers */
|
|
86
83
|
setChatUsage: (usage: ChatUsage | null) => void;
|
|
87
|
-
/** Switch the active chat user/session context. */
|
|
88
|
-
switchChatUserContext: (
|
|
89
|
-
userId: string | null,
|
|
90
|
-
hasPro: boolean,
|
|
91
|
-
options?: { clearPersistedCurrent?: boolean; restoreMessages?: boolean },
|
|
92
|
-
) => void;
|
|
93
84
|
}
|
|
94
85
|
|
|
95
86
|
export interface ChatUsage {
|
|
@@ -279,7 +270,7 @@ export const createChatSlice: StateCreator<ChatSlice, [], [], ChatSlice> = (set,
|
|
|
279
270
|
chatMessages: loadStoredMessages(null),
|
|
280
271
|
chatStatus: 'idle',
|
|
281
272
|
chatStreamingContent: '',
|
|
282
|
-
chatActiveModel: loadValidStoredModel(null,
|
|
273
|
+
chatActiveModel: loadValidStoredModel(null, hasAnyApiKeyAtInit()),
|
|
283
274
|
chatAutoExecute: loadStoredAutoExecute(),
|
|
284
275
|
chatError: null,
|
|
285
276
|
chatAbortController: null,
|
|
@@ -287,8 +278,7 @@ export const createChatSlice: StateCreator<ChatSlice, [], [], ChatSlice> = (set,
|
|
|
287
278
|
chatPendingPrompt: null,
|
|
288
279
|
chatPendingRepairRequest: null,
|
|
289
280
|
chatViewportScreenshot: null,
|
|
290
|
-
|
|
291
|
-
chatHasPro: false,
|
|
281
|
+
chatHasByokKey: false,
|
|
292
282
|
chatUsage: null,
|
|
293
283
|
chatStorageUserId: null,
|
|
294
284
|
|
|
@@ -340,12 +330,15 @@ export const createChatSlice: StateCreator<ChatSlice, [], [], ChatSlice> = (set,
|
|
|
340
330
|
setChatStreamingContent: (chatStreamingContent) => set({ chatStreamingContent }),
|
|
341
331
|
|
|
342
332
|
setChatActiveModel: (chatActiveModel) => {
|
|
343
|
-
|
|
333
|
+
// Accept any model the user picks — the ChatPanel shows an inline key
|
|
334
|
+
// prompt if the selected BYOK model doesn't have a key yet, and guards
|
|
335
|
+
// the send path. Coercion only happens on init and when keys are removed
|
|
336
|
+
// (via setChatHasByokKey).
|
|
344
337
|
try {
|
|
345
338
|
const key = getModelStorageKey(get().chatStorageUserId);
|
|
346
|
-
localStorage.setItem(key,
|
|
339
|
+
localStorage.setItem(key, chatActiveModel);
|
|
347
340
|
} catch { /* ignore */ }
|
|
348
|
-
set({ chatActiveModel
|
|
341
|
+
set({ chatActiveModel });
|
|
349
342
|
},
|
|
350
343
|
|
|
351
344
|
setChatAutoExecute: (chatAutoExecute) => {
|
|
@@ -404,51 +397,17 @@ export const createChatSlice: StateCreator<ChatSlice, [], [], ChatSlice> = (set,
|
|
|
404
397
|
|
|
405
398
|
setChatViewportScreenshot: (chatViewportScreenshot) => set({ chatViewportScreenshot }),
|
|
406
399
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
setChatHasPro: (chatHasPro) => {
|
|
410
|
-
const nextModel = coerceModelForEntitlement(get().chatActiveModel, chatHasPro);
|
|
400
|
+
setChatHasByokKey: (chatHasByokKey) => {
|
|
401
|
+
const nextModel = coerceModelForEntitlement(get().chatActiveModel, chatHasByokKey);
|
|
411
402
|
try {
|
|
412
403
|
const key = getModelStorageKey(get().chatStorageUserId);
|
|
413
404
|
localStorage.setItem(key, nextModel);
|
|
414
405
|
} catch { /* ignore */ }
|
|
415
|
-
set({
|
|
406
|
+
set({ chatHasByokKey, chatActiveModel: nextModel });
|
|
416
407
|
},
|
|
417
408
|
|
|
418
409
|
setChatUsage: (chatUsage) => set({ chatUsage }),
|
|
419
410
|
|
|
420
|
-
switchChatUserContext: (chatStorageUserId, chatHasPro, options) => {
|
|
421
|
-
const state = get();
|
|
422
|
-
state.chatAbortController?.abort();
|
|
423
|
-
if (options?.clearPersistedCurrent) {
|
|
424
|
-
try {
|
|
425
|
-
localStorage.removeItem(getMessagesStorageKey(state.chatStorageUserId));
|
|
426
|
-
} catch { /* ignore */ }
|
|
427
|
-
}
|
|
428
|
-
const restoredModel = coerceModelForEntitlement(
|
|
429
|
-
loadStoredModel(chatStorageUserId, state.chatActiveModel),
|
|
430
|
-
chatHasPro,
|
|
431
|
-
);
|
|
432
|
-
const restoredMessages = options?.restoreMessages === false
|
|
433
|
-
? []
|
|
434
|
-
: loadStoredMessages(chatStorageUserId);
|
|
435
|
-
set({
|
|
436
|
-
chatStorageUserId,
|
|
437
|
-
chatHasPro,
|
|
438
|
-
chatActiveModel: restoredModel,
|
|
439
|
-
chatMessages: restoredMessages,
|
|
440
|
-
chatStatus: 'idle',
|
|
441
|
-
chatStreamingContent: '',
|
|
442
|
-
chatError: null,
|
|
443
|
-
chatAbortController: null,
|
|
444
|
-
chatAttachments: [],
|
|
445
|
-
chatPendingPrompt: null,
|
|
446
|
-
chatPendingRepairRequest: null,
|
|
447
|
-
chatViewportScreenshot: null,
|
|
448
|
-
chatUsage: null,
|
|
449
|
-
});
|
|
450
|
-
},
|
|
451
|
-
|
|
452
411
|
sendErrorFeedback: (code, error) => {
|
|
453
412
|
const feedbackMessage: ChatMessage = {
|
|
454
413
|
id: crypto.randomUUID(),
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, beforeEach } from 'node:test';
|
|
6
6
|
import assert from 'node:assert';
|
|
7
|
-
import { createDataSlice, type DataSlice } from './dataSlice.js';
|
|
7
|
+
import { createDataSlice, type DataSlice, type DataCrossSliceState } from './dataSlice.js';
|
|
8
8
|
import { DATA_DEFAULTS } from '../constants.js';
|
|
9
9
|
|
|
10
|
+
type DataTestState = DataSlice & DataCrossSliceState;
|
|
11
|
+
|
|
10
12
|
// Mock mesh data for testing
|
|
11
13
|
const createMockMesh = (expressId: number, color: [number, number, number, number] = [1, 0, 0, 1]) => ({
|
|
12
14
|
expressId,
|
|
@@ -17,9 +19,16 @@ const createMockMesh = (expressId: number, color: [number, number, number, numbe
|
|
|
17
19
|
ifcType: 'IfcWall',
|
|
18
20
|
});
|
|
19
21
|
|
|
22
|
+
type TestSetState = (
|
|
23
|
+
partial:
|
|
24
|
+
| Partial<DataTestState>
|
|
25
|
+
| ((state: DataTestState) => Partial<DataTestState>),
|
|
26
|
+
) => void;
|
|
27
|
+
type TestGetState = () => DataTestState;
|
|
28
|
+
|
|
20
29
|
describe('DataSlice', () => {
|
|
21
|
-
let state:
|
|
22
|
-
let setState:
|
|
30
|
+
let state: DataTestState;
|
|
31
|
+
let setState: TestSetState;
|
|
23
32
|
|
|
24
33
|
beforeEach(() => {
|
|
25
34
|
setState = (partial) => {
|
|
@@ -31,7 +40,17 @@ describe('DataSlice', () => {
|
|
|
31
40
|
}
|
|
32
41
|
};
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
const getState: TestGetState = () => state;
|
|
44
|
+
|
|
45
|
+
// Seed the cross-slice fields owned by ModelSlice. dataSlice's
|
|
46
|
+
// updaters look up the active model in this map, so the test mock
|
|
47
|
+
// has to provide it for the typed StateCreator to be satisfiable.
|
|
48
|
+
const slice = createDataSlice(
|
|
49
|
+
setState as Parameters<typeof createDataSlice>[0],
|
|
50
|
+
getState as Parameters<typeof createDataSlice>[1],
|
|
51
|
+
undefined as unknown as Parameters<typeof createDataSlice>[2],
|
|
52
|
+
);
|
|
53
|
+
state = { ...slice, activeModelId: null, models: new Map() };
|
|
35
54
|
});
|
|
36
55
|
|
|
37
56
|
describe('initial state', () => {
|
|
@@ -20,7 +20,7 @@ import { DATA_DEFAULTS } from '../constants.js';
|
|
|
20
20
|
* consistent. The types below describe the minimal ModelSlice surface
|
|
21
21
|
* that dataSlice accesses through the merged Zustand state.
|
|
22
22
|
*/
|
|
23
|
-
interface DataCrossSliceState {
|
|
23
|
+
export interface DataCrossSliceState {
|
|
24
24
|
activeModelId: string | null;
|
|
25
25
|
models: Map<string, FederatedModel>;
|
|
26
26
|
}
|
|
@@ -4,16 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, beforeEach } from 'node:test';
|
|
6
6
|
import assert from 'node:assert';
|
|
7
|
-
import {
|
|
7
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
8
|
+
import type { GeometryResult } from '@ifc-lite/geometry';
|
|
9
|
+
import { createModelSlice, type ModelSlice, type ModelCrossSliceState } from './modelSlice.js';
|
|
8
10
|
import type { FederatedModel } from '../types.js';
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
type ModelTestState = ModelSlice & ModelCrossSliceState;
|
|
13
|
+
|
|
14
|
+
// Typed setter / getter shim that mirrors zustand's StateCreator
|
|
15
|
+
// signature without the broader middleware machinery the test doesn't
|
|
16
|
+
// need. Using StateCreator's exact types here would pull in the whole
|
|
17
|
+
// store; the local aliases below are tight enough for this test.
|
|
18
|
+
type TestSetState = (
|
|
19
|
+
partial:
|
|
20
|
+
| Partial<ModelTestState>
|
|
21
|
+
| ((state: ModelTestState) => Partial<ModelTestState>),
|
|
22
|
+
) => void;
|
|
23
|
+
type TestGetState = () => ModelTestState;
|
|
24
|
+
|
|
25
|
+
// Helper to create a mock model. `IfcDataStore` and `GeometryResult` are
|
|
26
|
+
// large interfaces that the slice never inspects on these paths — the
|
|
27
|
+
// double-cast through `unknown` is the minimum that satisfies the
|
|
28
|
+
// compiler without an `any`.
|
|
11
29
|
function createMockModel(id: string, name: string): FederatedModel {
|
|
12
30
|
return {
|
|
13
31
|
id,
|
|
14
32
|
name,
|
|
15
|
-
ifcDataStore: {} as
|
|
16
|
-
geometryResult: {} as
|
|
33
|
+
ifcDataStore: {} as unknown as IfcDataStore,
|
|
34
|
+
geometryResult: {} as unknown as GeometryResult,
|
|
17
35
|
visible: true,
|
|
18
36
|
collapsed: false,
|
|
19
37
|
schemaVersion: 'IFC4',
|
|
@@ -25,11 +43,10 @@ function createMockModel(id: string, name: string): FederatedModel {
|
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
describe('ModelSlice', () => {
|
|
28
|
-
let state:
|
|
29
|
-
let setState:
|
|
46
|
+
let state: ModelTestState;
|
|
47
|
+
let setState: TestSetState;
|
|
30
48
|
|
|
31
49
|
beforeEach(() => {
|
|
32
|
-
// Create a mock set function that updates state
|
|
33
50
|
setState = (partial) => {
|
|
34
51
|
if (typeof partial === 'function') {
|
|
35
52
|
const updates = partial(state);
|
|
@@ -39,8 +56,17 @@ describe('ModelSlice', () => {
|
|
|
39
56
|
}
|
|
40
57
|
};
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
const getState: TestGetState = () => state;
|
|
60
|
+
|
|
61
|
+
// The slice's StateCreator signature includes a third middleware
|
|
62
|
+
// argument (store API) that the slice's body never reads. We pass
|
|
63
|
+
// `undefined` cast to the empty middleware shape rather than `any`.
|
|
64
|
+
const slice = createModelSlice(
|
|
65
|
+
setState as Parameters<typeof createModelSlice>[0],
|
|
66
|
+
getState as Parameters<typeof createModelSlice>[1],
|
|
67
|
+
undefined as unknown as Parameters<typeof createModelSlice>[2],
|
|
68
|
+
);
|
|
69
|
+
state = { ...slice, ifcDataStore: null, geometryResult: null };
|
|
44
70
|
});
|
|
45
71
|
|
|
46
72
|
describe('initial state', () => {
|
|
@@ -270,4 +296,36 @@ describe('ModelSlice', () => {
|
|
|
270
296
|
assert.strictEqual(visible.length, 0);
|
|
271
297
|
});
|
|
272
298
|
});
|
|
299
|
+
|
|
300
|
+
describe('resolveGlobalIdFromModels — overlay-allocated ids', () => {
|
|
301
|
+
it('falls through to mutation views when the id is past maxExpressId', () => {
|
|
302
|
+
const model = createMockModel('model-1', 'First');
|
|
303
|
+
model.idOffset = 0;
|
|
304
|
+
model.maxExpressId = 10_000;
|
|
305
|
+
state.addModel(model);
|
|
306
|
+
|
|
307
|
+
// Seed a fake mutation view with a fresh overlay entity. The
|
|
308
|
+
// resolver only reads `getNewEntity` from each view, so we type
|
|
309
|
+
// the map narrowly and let it satisfy the slice's wider type via
|
|
310
|
+
// a single-property cast on the wrapping state object.
|
|
311
|
+
type StubView = { getNewEntity: (id: number) => { expressId: number } | null };
|
|
312
|
+
const stubViews: Map<string, StubView> = new Map([
|
|
313
|
+
['model-1', { getNewEntity: (id: number) => (id === 11_001 ? { expressId: id } : null) }],
|
|
314
|
+
]);
|
|
315
|
+
state = { ...state, mutationViews: stubViews } as typeof state & { mutationViews: Map<string, StubView> };
|
|
316
|
+
|
|
317
|
+
// Inside the parsed range — first pass resolves it.
|
|
318
|
+
const within = state.resolveGlobalIdFromModels(42);
|
|
319
|
+
assert.deepStrictEqual(within, { modelId: 'model-1', expressId: 42 });
|
|
320
|
+
|
|
321
|
+
// Above the parsed range but in the overlay — second pass resolves it.
|
|
322
|
+
const overlay = state.resolveGlobalIdFromModels(11_001);
|
|
323
|
+
assert.deepStrictEqual(overlay, { modelId: 'model-1', expressId: 11_001 });
|
|
324
|
+
|
|
325
|
+
// Above the parsed range and NOT in the overlay — returns null
|
|
326
|
+
// so callers can fall back to the legacy single-model path.
|
|
327
|
+
const phantom = state.resolveGlobalIdFromModels(99_999);
|
|
328
|
+
assert.strictEqual(phantom, null);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
273
331
|
});
|
|
@@ -13,8 +13,20 @@
|
|
|
13
13
|
|
|
14
14
|
import type { StateCreator } from 'zustand';
|
|
15
15
|
import type { FederatedModel } from '../types.js';
|
|
16
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
17
|
+
import type { GeometryResult } from '@ifc-lite/geometry';
|
|
16
18
|
import { federationRegistry, type GlobalIdLookup } from '@ifc-lite/renderer';
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Cross-slice fields the model actions write to. `ifcDataStore` and
|
|
22
|
+
* `geometryResult` are owned by `dataSlice` but `modelSlice`'s set()
|
|
23
|
+
* calls need to keep them in sync with the active model.
|
|
24
|
+
*/
|
|
25
|
+
export interface ModelCrossSliceState {
|
|
26
|
+
ifcDataStore: IfcDataStore | null;
|
|
27
|
+
geometryResult: GeometryResult | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export interface ModelSlice {
|
|
19
31
|
// State
|
|
20
32
|
/** Map of all loaded models by ID */
|
|
@@ -72,7 +84,7 @@ export interface ModelSlice {
|
|
|
72
84
|
resolveGlobalIdFromModels: (globalId: number) => GlobalIdLookup | null;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
export const createModelSlice: StateCreator<ModelSlice, [], [], ModelSlice> = (set, get) => ({
|
|
87
|
+
export const createModelSlice: StateCreator<ModelSlice & ModelCrossSliceState, [], [], ModelSlice> = (set, get) => ({
|
|
76
88
|
// Initial state
|
|
77
89
|
models: new Map(),
|
|
78
90
|
activeModelId: null,
|
|
@@ -245,19 +257,39 @@ export const createModelSlice: StateCreator<ModelSlice, [], [], ModelSlice> = (s
|
|
|
245
257
|
*/
|
|
246
258
|
resolveGlobalIdFromModels: (globalId: number) => {
|
|
247
259
|
const models = get().models;
|
|
260
|
+
const mutationViews = (get() as unknown as { mutationViews?: Map<string, { getNewEntity: (id: number) => unknown }> }).mutationViews;
|
|
248
261
|
|
|
249
262
|
// Sort models by offset for correct range checking
|
|
250
263
|
const sortedModels = Array.from(models.values()).sort((a, b) => a.idOffset - b.idOffset);
|
|
251
264
|
|
|
252
|
-
// Find the model that contains this globalId
|
|
253
|
-
//
|
|
265
|
+
// Find the model that contains this globalId.
|
|
266
|
+
//
|
|
267
|
+
// First pass — parse-time range. A model owns ids in
|
|
268
|
+
// `[offset, offset + maxExpressId]` from the original parse. This
|
|
269
|
+
// is the fast path covering 99% of selections.
|
|
270
|
+
//
|
|
271
|
+
// Second pass — overlay-allocated ids. Duplicates / scripted adds
|
|
272
|
+
// through StoreEditor land ABOVE the model's parse-time
|
|
273
|
+
// maxExpressId, so they fall outside the first-pass range. The
|
|
274
|
+
// federation resolver knows nothing about overlay state, so we
|
|
275
|
+
// consult each model's mutation view for the freshly-added
|
|
276
|
+
// entity. Falls back gracefully when no view is registered.
|
|
254
277
|
for (const model of sortedModels) {
|
|
255
278
|
const localId = globalId - model.idOffset;
|
|
256
279
|
if (localId >= 0 && localId <= model.maxExpressId) {
|
|
257
|
-
return {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
280
|
+
return { modelId: model.id, expressId: localId };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (mutationViews) {
|
|
285
|
+
for (const model of sortedModels) {
|
|
286
|
+
const localId = globalId - model.idOffset;
|
|
287
|
+
if (localId <= model.maxExpressId) continue; // already covered above
|
|
288
|
+
const view = mutationViews.get(model.id);
|
|
289
|
+
if (!view) continue;
|
|
290
|
+
if (view.getNewEntity(localId) !== null) {
|
|
291
|
+
return { modelId: model.id, expressId: localId };
|
|
292
|
+
}
|
|
261
293
|
}
|
|
262
294
|
}
|
|
263
295
|
|