@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,102 @@
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
+ * ModelSelector — dropdown to pick the LLM model.
7
+ * Free models available to everyone. Pro models show cost indicator and lock icon.
8
+ */
9
+
10
+ import { useCallback } from 'react';
11
+ import { Lock } from 'lucide-react';
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '@/components/ui/select';
19
+ import { useViewerStore } from '@/store';
20
+ import { FREE_MODELS, PRO_MODELS, getModelById } from '@/lib/llm/models';
21
+
22
+ interface ModelSelectorProps {
23
+ /** Whether the user has a pro subscription */
24
+ hasPro?: boolean;
25
+ }
26
+
27
+ function formatContextWindow(tokens: number): string {
28
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(0)}M`;
29
+ return `${(tokens / 1_000).toFixed(0)}K`;
30
+ }
31
+
32
+ export function ModelSelector({ hasPro = false }: ModelSelectorProps) {
33
+ const activeModel = useViewerStore((s) => s.chatActiveModel);
34
+ const setActiveModel = useViewerStore((s) => s.setChatActiveModel);
35
+
36
+ const handleChange = useCallback((value: string) => {
37
+ setActiveModel(value);
38
+ }, [setActiveModel]);
39
+
40
+ const current = getModelById(activeModel);
41
+
42
+ return (
43
+ <Select value={activeModel} onValueChange={handleChange}>
44
+ <SelectTrigger className="h-6 text-xs w-auto min-w-[140px] gap-1 border-0 bg-transparent hover:bg-muted/50">
45
+ <SelectValue>
46
+ <span className="truncate flex items-center gap-1">
47
+ {current?.name ?? activeModel}
48
+ {current?.cost && (
49
+ <span className={`text-[10px] font-mono ${
50
+ current.cost === '$$$' ? 'text-amber-500' : current.cost === '$$' ? 'text-blue-500' : 'text-emerald-500'
51
+ }`}>
52
+ {current.cost}
53
+ </span>
54
+ )}
55
+ </span>
56
+ </SelectValue>
57
+ </SelectTrigger>
58
+ <SelectContent>
59
+ {/* Free tier */}
60
+ <div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
61
+ Free
62
+ </div>
63
+ {FREE_MODELS.map((m) => (
64
+ <SelectItem key={m.id} value={m.id} className="text-xs">
65
+ <span className="flex items-center gap-1.5">
66
+ <span>{m.name}</span>
67
+ <span className="text-muted-foreground text-[10px]">{m.provider}</span>
68
+ <span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
69
+ </span>
70
+ </SelectItem>
71
+ ))}
72
+
73
+ {/* Pro tier */}
74
+ <div className="px-2 py-1 mt-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1">
75
+ Pro
76
+ </div>
77
+ {PRO_MODELS.map((m) => (
78
+ <SelectItem
79
+ key={m.id}
80
+ value={m.id}
81
+ disabled={!hasPro}
82
+ className="text-xs"
83
+ >
84
+ <span className="flex items-center gap-1.5">
85
+ <span>{m.name}</span>
86
+ <span className="text-muted-foreground text-[10px]">{m.provider}</span>
87
+ {m.cost && (
88
+ <span className={`text-[10px] font-mono ${
89
+ m.cost === '$$$' ? 'text-amber-500' : m.cost === '$$' ? 'text-blue-500' : 'text-emerald-500'
90
+ }`}>
91
+ {m.cost}
92
+ </span>
93
+ )}
94
+ <span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
95
+ {!hasPro && <Lock className="h-3 w-3 text-muted-foreground/50" />}
96
+ </span>
97
+ </SelectItem>
98
+ ))}
99
+ </SelectContent>
100
+ </Select>
101
+ );
102
+ }
@@ -0,0 +1,23 @@
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 { renderTextContent } from './renderTextContent';
8
+
9
+ test('escapes html tags and script payloads', () => {
10
+ const rendered = renderTextContent('<img src=x onerror=alert(1)><script>alert("xss")</script>');
11
+ assert.ok(rendered.includes('&lt;img src=x onerror=alert(1)&gt;'));
12
+ assert.ok(rendered.includes('&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'));
13
+ assert.ok(!rendered.includes('<script>'));
14
+ assert.ok(!rendered.includes('<img'));
15
+ });
16
+
17
+ test('preserves allowed inline markdown after escaping', () => {
18
+ const rendered = renderTextContent('**bold** *italic* `const x = 1`');
19
+ assert.equal(
20
+ rendered,
21
+ '<strong>bold</strong> <em>italic</em> <code class="bg-muted px-1 py-0.5 rounded text-xs font-mono">const x = 1</code>',
22
+ );
23
+ });
@@ -0,0 +1,19 @@
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
+ /** Simple markdown-ish rendering for text segments with HTML escaping. */
6
+ export function renderTextContent(text: string): string {
7
+ const escaped = text
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
13
+
14
+ return escaped
15
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
16
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
17
+ .replace(/`([^`]+)`/g, '<code class="bg-muted px-1 py-0.5 rounded text-xs font-mono">$1</code>')
18
+ .replace(/\n/g, '<br/>');
19
+ }
@@ -68,7 +68,8 @@ export function HierarchyNode({
68
68
  onRemoveModel,
69
69
  onModelHeaderClick,
70
70
  }: HierarchyNodeProps) {
71
- const Icon = TYPE_ICONS[node.type] || TYPE_ICONS.default;
71
+ const resolvedType = node.ifcType || node.type;
72
+ const Icon = TYPE_ICONS[resolvedType] || TYPE_ICONS[node.type] || TYPE_ICONS.default;
72
73
 
73
74
  // Model header nodes (for visibility control and expansion)
74
75
  if (node.type === 'model-header' && node.id.startsWith('model-')) {
@@ -261,19 +262,25 @@ export function HierarchyNode({
261
262
  <Icon className="h-3.5 w-3.5 shrink-0 text-zinc-500 dark:text-zinc-400" />
262
263
  </TooltipTrigger>
263
264
  <TooltipContent>
264
- <p className="text-xs">{node.type}</p>
265
+ <p className="text-xs">{resolvedType}</p>
265
266
  </TooltipContent>
266
267
  </Tooltip>
267
268
 
268
269
  {/* Name */}
269
270
  <span className={cn(
270
271
  'flex-1 text-sm truncate ml-1.5',
271
- isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'unified-storey' || node.type === 'type-group'
272
+ isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' || node.type === 'type-group'
272
273
  ? 'font-medium text-zinc-900 dark:text-zinc-100'
273
274
  : 'text-zinc-700 dark:text-zinc-300',
274
275
  nodeHidden && 'line-through decoration-zinc-400 dark:decoration-zinc-600'
275
276
  )}>{node.name}</span>
276
277
 
278
+ {node.ifcType && node.type === 'element' && (
279
+ <span className="text-[10px] font-mono text-zinc-400 dark:text-zinc-500 truncate max-w-[90px]">
280
+ {node.ifcType}
281
+ </span>
282
+ )}
283
+
277
284
  {/* Storey Elevation */}
278
285
  {node.storeyElevation !== undefined && (
279
286
  <Tooltip>
@@ -0,0 +1,126 @@
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 { describe, it } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import { IfcTypeEnum, type SpatialHierarchy, type SpatialNode } from '@ifc-lite/data';
8
+ import type { IfcDataStore } from '@ifc-lite/parser';
9
+ import { useViewerStore, type FederatedModel } from '@/store';
10
+ import { buildTreeData } from './treeDataBuilder';
11
+
12
+ function createSpatialNode(
13
+ expressId: number,
14
+ type: IfcTypeEnum,
15
+ name: string,
16
+ children: SpatialNode[] = [],
17
+ ): SpatialNode {
18
+ return {
19
+ expressId,
20
+ type,
21
+ name,
22
+ children,
23
+ elements: [],
24
+ };
25
+ }
26
+
27
+ function createDataStore(): IfcDataStore {
28
+ const spaceNode = createSpatialNode(5, IfcTypeEnum.IfcSpace, 'e3035b71');
29
+ const storeyNode = createSpatialNode(4, IfcTypeEnum.IfcBuildingStorey, 'MY_STOREY', [spaceNode]);
30
+ const buildingNode = createSpatialNode(3, IfcTypeEnum.IfcBuilding, 'MY_BUILDING', [storeyNode]);
31
+ const siteNode = createSpatialNode(2, IfcTypeEnum.IfcSite, 'MY_SITE', [buildingNode]);
32
+ const projectNode = createSpatialNode(1, IfcTypeEnum.IfcProject, 'MY_PROJECT', [siteNode]);
33
+
34
+ const spatialHierarchy: SpatialHierarchy = {
35
+ project: projectNode,
36
+ byStorey: new Map([[4, [6, 7]]]),
37
+ byBuilding: new Map(),
38
+ bySite: new Map(),
39
+ bySpace: new Map([[5, [7]]]),
40
+ storeyElevations: new Map(),
41
+ storeyHeights: new Map(),
42
+ elementToStorey: new Map([[6, 4], [7, 4]]),
43
+ getStoreyElements: () => [],
44
+ getStoreyByElevation: () => null,
45
+ getContainingSpace: (elementId: number) => (elementId === 7 ? 5 : null),
46
+ getPath: () => [],
47
+ };
48
+
49
+ return {
50
+ spatialHierarchy,
51
+ entities: {
52
+ count: 0,
53
+ getName: (id: number) => {
54
+ if (id === 6) return 'Wall';
55
+ if (id === 7) return '';
56
+ return '';
57
+ },
58
+ getTypeName: (id: number) => {
59
+ if (id === 6) return 'IfcWall';
60
+ if (id === 7) return 'IfcWindow';
61
+ if (id === 5) return 'IfcSpace';
62
+ return 'Unknown';
63
+ },
64
+ },
65
+ } as unknown as IfcDataStore;
66
+ }
67
+
68
+ function createModel(idOffset: number): FederatedModel {
69
+ return {
70
+ id: 'model-1',
71
+ name: 'Model 1',
72
+ ifcDataStore: createDataStore(),
73
+ geometryResult: { meshes: [], totalVertices: 0, totalTriangles: 0, coordinateInfo: null as never },
74
+ visible: true,
75
+ collapsed: false,
76
+ schemaVersion: 'IFC4',
77
+ loadedAt: 1,
78
+ fileSize: 1,
79
+ idOffset,
80
+ maxExpressId: 7,
81
+ };
82
+ }
83
+
84
+ describe('buildTreeData', () => {
85
+ it('keeps IfcSpace as a spatial node, expands bySpace children, and avoids storey duplicates', () => {
86
+ useViewerStore.setState({ models: new Map() });
87
+ useViewerStore.getState().registerModelOffset('tree-test-padding', 99);
88
+ const idOffset = useViewerStore.getState().registerModelOffset('model-1', 7);
89
+ const model = createModel(idOffset);
90
+ useViewerStore.setState({ models: new Map([['model-1', model]]) });
91
+
92
+ const models = new Map<string, FederatedModel>([['model-1', model]]);
93
+ const expandedNodes = new Set([
94
+ 'root-1',
95
+ 'root-1-2',
96
+ 'root-1-2-3',
97
+ 'root-1-2-3-4',
98
+ 'root-1-2-3-4-5',
99
+ ]);
100
+
101
+ const nodes = buildTreeData(models, null, expandedNodes, false, []);
102
+
103
+ const storeyNode = nodes.find((node) => node.id === 'root-1-2-3-4');
104
+ assert.ok(storeyNode);
105
+ assert.strictEqual(storeyNode.elementCount, 1);
106
+
107
+ const spaceNode = nodes.find((node) => node.id === 'root-1-2-3-4-5');
108
+ assert.ok(spaceNode);
109
+ assert.strictEqual(spaceNode.type, 'IfcSpace');
110
+ assert.deepStrictEqual(spaceNode.expressIds, [5]);
111
+ assert.deepStrictEqual(spaceNode.globalIds, [105]);
112
+ assert.strictEqual(spaceNode.elementCount, 1);
113
+ assert.strictEqual(spaceNode.hasChildren, true);
114
+
115
+ const windowNode = nodes.find((node) => node.id === 'element-model-1-7');
116
+ assert.ok(windowNode);
117
+ assert.strictEqual(windowNode.type, 'element');
118
+ assert.strictEqual(windowNode.ifcType, 'IfcWindow');
119
+ assert.deepStrictEqual(windowNode.expressIds, [7]);
120
+ assert.deepStrictEqual(windowNode.globalIds, [107]);
121
+ assert.strictEqual(windowNode.name, 'IfcWindow #7');
122
+
123
+ assert.strictEqual(nodes.filter((node) => node.id === 'element-model-1-6').length, 1);
124
+ assert.strictEqual(nodes.filter((node) => node.id === 'element-model-1-7').length, 1);
125
+ });
126
+ });