@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.
- package/CHANGELOG.md +35 -0
- package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
- package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
- package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-CMQ_Dgkr.css +1 -0
- package/dist/assets/index-D7nEDctQ.js +229 -0
- package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
- package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
- package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +21 -20
- package/src/App.tsx +17 -1
- package/src/components/viewer/BasketPresentationDock.tsx +8 -4
- package/src/components/viewer/ChatPanel.tsx +1402 -0
- package/src/components/viewer/CodeEditor.tsx +70 -4
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/HierarchyPanel.tsx +28 -13
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/ScriptPanel.tsx +351 -184
- package/src/components/viewer/UpgradePage.tsx +69 -0
- package/src/components/viewer/Viewport.tsx +23 -0
- package/src/components/viewer/chat/ChatMessage.tsx +144 -0
- package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
- package/src/components/viewer/chat/ModelSelector.tsx +102 -0
- package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
- package/src/components/viewer/chat/renderTextContent.ts +19 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/src/hooks/useIfcCache.ts +1 -2
- package/src/hooks/useSandbox.ts +122 -6
- package/src/index.css +10 -0
- package/src/lib/attachments.ts +46 -0
- package/src/lib/llm/ClerkChatSync.tsx +74 -0
- package/src/lib/llm/clerk-auth.ts +62 -0
- package/src/lib/llm/code-extractor.ts +50 -0
- package/src/lib/llm/context-builder.test.ts +18 -0
- package/src/lib/llm/context-builder.ts +305 -0
- package/src/lib/llm/free-models.test.ts +118 -0
- package/src/lib/llm/message-capabilities.test.ts +131 -0
- package/src/lib/llm/message-capabilities.ts +94 -0
- package/src/lib/llm/models.ts +197 -0
- package/src/lib/llm/repair-loop.test.ts +91 -0
- package/src/lib/llm/repair-loop.ts +76 -0
- package/src/lib/llm/script-diagnostics.ts +445 -0
- package/src/lib/llm/script-edit-ops.test.ts +399 -0
- package/src/lib/llm/script-edit-ops.ts +954 -0
- package/src/lib/llm/script-preflight.test.ts +513 -0
- package/src/lib/llm/script-preflight.ts +990 -0
- package/src/lib/llm/script-preservation.test.ts +128 -0
- package/src/lib/llm/script-preservation.ts +152 -0
- package/src/lib/llm/stream-client.test.ts +97 -0
- package/src/lib/llm/stream-client.ts +410 -0
- package/src/lib/llm/system-prompt.test.ts +181 -0
- package/src/lib/llm/system-prompt.ts +665 -0
- package/src/lib/llm/types.ts +150 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
- package/src/lib/scripts/templates/create-building.ts +12 -12
- package/src/main.tsx +10 -1
- package/src/sdk/adapters/export-adapter.test.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +40 -16
- package/src/sdk/adapters/files-adapter.ts +39 -0
- package/src/sdk/adapters/model-compat.ts +1 -1
- package/src/sdk/adapters/mutate-adapter.ts +20 -6
- package/src/sdk/adapters/mutation-view.ts +112 -0
- package/src/sdk/adapters/query-adapter.ts +100 -4
- package/src/sdk/local-backend.ts +4 -0
- package/src/store/index.ts +15 -1
- package/src/store/slices/chatSlice.test.ts +325 -0
- package/src/store/slices/chatSlice.ts +468 -0
- package/src/store/slices/scriptSlice.test.ts +75 -0
- package/src/store/slices/scriptSlice.ts +256 -9
- package/src/vite-env.d.ts +10 -0
- package/vite.config.ts +21 -2
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/index-ByrFvN5A.css +0 -1
- 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('<img src=x onerror=alert(1)>'));
|
|
12
|
+
assert.ok(rendered.includes('<script>alert("xss")</script>'));
|
|
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, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, ''');
|
|
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
|
|
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">{
|
|
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
|
+
});
|