@ifc-lite/viewer 1.23.0 → 1.25.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 +34 -31
- package/CHANGELOG.md +96 -0
- package/dist/assets/{basketViewActivator-Dn_bHUl2.js → basketViewActivator-CU8_toGq.js} +7 -7
- package/dist/assets/{bcf-B9SFl84i.js → bcf-DXGDhw56.js} +23 -23
- package/dist/assets/{deflate-yMpdCIqk.js → deflate-Bb1_H2Yf.js} +1 -1
- package/dist/assets/{exporters-D-BvrNIg.js → exporters-DZhLN0ux.js} +1861 -1658
- package/dist/assets/geometry-controller.worker-DQOSYqtw.js +7 -0
- package/dist/assets/geometry.worker-B62e03Ao.js +1 -0
- package/dist/assets/{geotiff-D1tvcDCb.js → geotiff-y0ZxbRJd.js} +10 -10
- package/dist/assets/{ids-DZLs0snJ.js → ids-DruUNtfD.js} +4 -4
- package/dist/assets/ifc-lite-Ch2T9pP9.js +7 -0
- package/dist/assets/{ifc-lite_bg-DyHX37GQ.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-BIryVCXQ.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
- package/dist/assets/index-Bws3UAkj.css +1 -0
- package/dist/assets/{index-CXSBhkcJ.js → index-Dr88ZlSY.js} +64100 -47030
- package/dist/assets/{jpeg-DUMcZp24.js → jpeg-B3_loqFe.js} +1 -1
- package/dist/assets/lens-PYsLu_MA.js +1 -0
- package/dist/assets/{lerc-IN4uWojP.js → lerc-nkwS8ZUe.js} +1 -1
- package/dist/assets/{lzw-Cnw0hH-m.js → lzw-D3cW5Wpg.js} +1 -1
- package/dist/assets/{native-bridge-BVf2uzoH.js → native-bridge-BcYJooq8.js} +2 -2
- package/dist/assets/{packbits-BskJCwk0.js → packbits-DDN4xzB5.js} +1 -1
- package/dist/assets/{parser.worker-BdtkkaGf.js → parser.worker-BW1IMUed.js} +3 -3
- package/dist/assets/raw-CoIXstQ-.js +1 -0
- package/dist/assets/{sandbox-VLI_y7cl.js → sandbox-DETNEyQb.js} +498 -470
- package/dist/assets/{server-client-BLcKaWQB.js → server-client-CmzJOeS7.js} +1 -1
- package/dist/assets/{wasm-bridge-BAfZh7YT.js → wasm-bridge-CT7mK9W0.js} +1 -1
- package/dist/assets/{webimage-Db2xzze3.js → webimage-CBjgg4up.js} +1 -1
- package/dist/assets/{workerHelpers--sAYm9yN.js → workerHelpers-IEQDo8r3.js} +1 -1
- package/dist/assets/{zstd-BDToOQyD.js → zstd-C8oQ6qdS.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +11 -9
- package/src/App.tsx +5 -2
- package/src/components/extensions/AuditLogPanel.tsx +259 -0
- package/src/components/extensions/BundlePreview.tsx +102 -0
- package/src/components/extensions/CapabilityReview.tsx +333 -0
- package/src/components/extensions/ExtensionDockHost.tsx +192 -0
- package/src/components/extensions/ExtensionToolbarSlot.tsx +106 -0
- package/src/components/extensions/ExtensionsPanel.tsx +481 -0
- package/src/components/extensions/FlavorDialog.tsx +398 -0
- package/src/components/extensions/FlavorImportPreview.tsx +79 -0
- package/src/components/extensions/FlavorIndicator.tsx +81 -0
- package/src/components/extensions/FlavorListView.tsx +318 -0
- package/src/components/extensions/FlavorMergeDialog.tsx +326 -0
- package/src/components/extensions/HelpHint.tsx +182 -0
- package/src/components/extensions/IdeasPanel.tsx +344 -0
- package/src/components/extensions/PlanCard.tsx +227 -0
- package/src/components/extensions/PrivacyPanel.tsx +312 -0
- package/src/components/extensions/PromoteToolDialog.tsx +313 -0
- package/src/components/extensions/RepairQueuePanel.tsx +222 -0
- package/src/components/extensions/icon-registry.ts +92 -0
- package/src/components/extensions/toast-helpers.ts +49 -0
- package/src/components/extensions/widget/WidgetErrorBoundary.tsx +62 -0
- package/src/components/extensions/widget/WidgetRenderer.tsx +428 -0
- package/src/components/viewer/ChatPanel.tsx +251 -3
- package/src/components/viewer/CommandPalette.tsx +74 -4
- package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
- package/src/components/viewer/EntityContextMenu.tsx +70 -0
- package/src/components/viewer/ExportDialog.tsx +9 -1
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +21 -6
- package/src/components/viewer/LensPanel.tsx +50 -0
- package/src/components/viewer/MainToolbar.tsx +170 -87
- package/src/components/viewer/ScriptPanel.tsx +105 -1
- package/src/components/viewer/Section2DPanel.tsx +58 -2
- package/src/components/viewer/StatusBar.tsx +18 -0
- package/src/components/viewer/ViewerLayout.tsx +53 -4
- package/src/components/viewer/Viewport.tsx +72 -0
- package/src/hooks/useActionLogger.test.ts +161 -0
- package/src/hooks/useActionLogger.ts +141 -0
- package/src/hooks/useForkExtension.ts +51 -0
- package/src/hooks/useIfcFederation.ts +7 -1
- package/src/hooks/useInstalledExtensions.ts +43 -0
- package/src/hooks/usePrivacyDisclosure.ts +48 -0
- package/src/hooks/useRunExtensionTests.ts +67 -0
- package/src/hooks/useSlotContributions.ts +38 -0
- package/src/hooks/useSymbolicAnnotations.test.ts +124 -0
- package/src/hooks/useSymbolicAnnotations.ts +776 -0
- package/src/lib/desktop-product.ts +7 -1
- package/src/lib/lens/adapter.ts +14 -0
- package/src/lib/llm/prompt-cache.ts +77 -0
- package/src/lib/llm/stream-client.ts +20 -2
- package/src/lib/llm/stream-direct.ts +11 -1
- package/src/lib/llm/system-prompt.ts +42 -0
- package/src/lib/safe-mode.ts +30 -0
- package/src/sdk/ExtensionHostProvider.tsx +103 -0
- package/src/services/extensions/flavor-service.ts +183 -0
- package/src/services/extensions/host-commands.ts +112 -0
- package/src/services/extensions/host-installer.ts +289 -0
- package/src/services/extensions/host.ts +514 -0
- package/src/services/extensions/idb-flavor-storage.test.ts +140 -0
- package/src/services/extensions/idb-flavor-storage.ts +241 -0
- package/src/services/extensions/idb-log-storage.test.ts +110 -0
- package/src/services/extensions/idb-log-storage.ts +171 -0
- package/src/services/extensions/idb-storage.ts +228 -0
- package/src/services/extensions/runtime-errors.ts +26 -0
- package/src/services/extensions/sandbox-factory.ts +217 -0
- package/src/store/constants.ts +48 -6
- package/src/store/index.ts +6 -1
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/extensionsSlice.ts +90 -0
- package/src/store/slices/lensSlice.ts +28 -0
- package/src/store/slices/visibilitySlice.test.ts +6 -0
- package/src/store/slices/visibilitySlice.ts +17 -8
- package/src/store/types.ts +2 -0
- package/dist/assets/geometry-controller.worker-Cm5pvyR6.js +0 -7
- package/dist/assets/geometry.worker-ClNvXIrj.js +0 -1
- package/dist/assets/ifc-lite-BDg0iIbj.js +0 -7
- package/dist/assets/index-DS_xJQfP.css +0 -1
- package/dist/assets/lens-CpjUdqpw.js +0 -1
- package/dist/assets/raw-DzTtEZIY.js +0 -1
|
@@ -24,11 +24,9 @@ import {
|
|
|
24
24
|
ArrowRight,
|
|
25
25
|
Box,
|
|
26
26
|
HelpCircle,
|
|
27
|
-
Sparkles,
|
|
28
27
|
Loader2,
|
|
29
28
|
Camera,
|
|
30
29
|
Info,
|
|
31
|
-
Layers,
|
|
32
30
|
Layers2,
|
|
33
31
|
SquareX,
|
|
34
32
|
Building2,
|
|
@@ -36,6 +34,7 @@ import {
|
|
|
36
34
|
PackagePlus,
|
|
37
35
|
MessageSquare,
|
|
38
36
|
ClipboardCheck,
|
|
37
|
+
Puzzle,
|
|
39
38
|
Palette,
|
|
40
39
|
Orbit,
|
|
41
40
|
Layout,
|
|
@@ -85,6 +84,7 @@ import { useFloorplanView } from '@/hooks/useFloorplanView';
|
|
|
85
84
|
import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess, type DesktopFeature } from '@/lib/desktop-product';
|
|
86
85
|
import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
|
|
87
86
|
import { ThemeSwitch } from './ThemeSwitch';
|
|
87
|
+
import { ExtensionToolbarSlot } from '@/components/extensions/ExtensionToolbarSlot';
|
|
88
88
|
import { toast } from '@/components/ui/toast';
|
|
89
89
|
import { navigateToPath } from '@/services/app-navigation';
|
|
90
90
|
import { getStartupHarnessRequest, setActiveHarnessRequest, tryClaimStartupHarnessRequest } from '@/services/desktop-harness';
|
|
@@ -497,6 +497,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
497
497
|
const editEnabled = useViewerStore((state) => state.editEnabled);
|
|
498
498
|
const toggleEditEnabled = useViewerStore((state) => state.toggleEditEnabled);
|
|
499
499
|
const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
|
|
500
|
+
const selectedEntityIds = useViewerStore((state) => state.selectedEntityIds);
|
|
500
501
|
const hideEntities = useViewerStore((state) => state.hideEntities);
|
|
501
502
|
const error = useViewerStore((state) => state.error);
|
|
502
503
|
const cameraCallbacks = useViewerStore((state) => state.cameraCallbacks);
|
|
@@ -528,6 +529,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
528
529
|
// Lens state
|
|
529
530
|
const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
|
|
530
531
|
const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
|
|
532
|
+
const extensionsPanelVisible = useViewerStore((state) => state.extensionsPanelVisible);
|
|
533
|
+
const setExtensionsPanelVisible = useViewerStore((state) => state.setExtensionsPanelVisible);
|
|
531
534
|
const scriptPanelVisible = useViewerStore((state) => state.scriptPanelVisible);
|
|
532
535
|
const setScriptPanelVisible = useViewerStore((state) => state.setScriptPanelVisible);
|
|
533
536
|
const ganttPanelVisible = useViewerStore((state) => state.ganttPanelVisible);
|
|
@@ -624,6 +627,30 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
624
627
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- meshLen is a stable proxy for geometryResult
|
|
625
628
|
}, [models, meshLen]);
|
|
626
629
|
|
|
630
|
+
// IfcAnnotation has no body mesh, so it can't be detected via the mesh scan.
|
|
631
|
+
// Look up the entity table directly. byType keys are uppercase STEP names
|
|
632
|
+
// ('IFCANNOTATION') but cache loads sometimes preserve PascalCase too.
|
|
633
|
+
// Symbolic 2D overlays cover BOTH IfcAnnotation (text, dimensions, leader
|
|
634
|
+
// lines, filled regions) AND IfcGrid (axis lines + synthesized bubble +
|
|
635
|
+
// tag). Some files ship only grids (Snowdon Towers Structural is the
|
|
636
|
+
// canonical example — no IfcAnnotation at all), so the toggle must
|
|
637
|
+
// surface for either entity type or grid-only models get no way to hide
|
|
638
|
+
// the overlay.
|
|
639
|
+
const hasIfcAnnotations = useMemo(() => {
|
|
640
|
+
const has = (store: typeof ifcDataStore | undefined) => {
|
|
641
|
+
const byType = store?.entityIndex?.byType;
|
|
642
|
+
if (!byType) return false;
|
|
643
|
+
return (byType.get('IFCANNOTATION')?.length ?? 0) > 0
|
|
644
|
+
|| (byType.get('IfcAnnotation')?.length ?? 0) > 0
|
|
645
|
+
|| (byType.get('IFCGRID')?.length ?? 0) > 0
|
|
646
|
+
|| (byType.get('IfcGrid')?.length ?? 0) > 0;
|
|
647
|
+
};
|
|
648
|
+
if (models.size > 0) {
|
|
649
|
+
for (const [, m] of models) if (has(m.ifcDataStore)) return true;
|
|
650
|
+
}
|
|
651
|
+
return has(ifcDataStore);
|
|
652
|
+
}, [models, ifcDataStore]);
|
|
653
|
+
|
|
627
654
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
628
655
|
const files = e.target.files;
|
|
629
656
|
if (!files || files.length === 0) return;
|
|
@@ -699,6 +726,12 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
699
726
|
}, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
|
|
700
727
|
|
|
701
728
|
const hasSelection = selectedEntityId !== null;
|
|
729
|
+
// Selection chip uses the multi-select size when present; falls back
|
|
730
|
+
// to the single legacy `selectedEntityId` so the chip still says
|
|
731
|
+
// "1 selected" for the click-to-pick flow that hasn't migrated.
|
|
732
|
+
const selectionCount = selectedEntityIds.size > 0
|
|
733
|
+
? selectedEntityIds.size
|
|
734
|
+
: (selectedEntityId !== null ? 1 : 0);
|
|
702
735
|
|
|
703
736
|
const clearSelection = useViewerStore((state) => state.clearSelection);
|
|
704
737
|
|
|
@@ -765,7 +798,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
765
798
|
setScriptPanelVisible,
|
|
766
799
|
]);
|
|
767
800
|
|
|
768
|
-
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'addElement') => {
|
|
801
|
+
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'addElement' | 'extensions') => {
|
|
769
802
|
if (activeAnalysisExtension?.placement !== 'bottom') {
|
|
770
803
|
closeActiveAnalysisExtension();
|
|
771
804
|
}
|
|
@@ -775,16 +808,21 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
775
808
|
if (panel === 'ids' && !requireDesktopFeature('ids_validation', 'IDS validation')) {
|
|
776
809
|
return;
|
|
777
810
|
}
|
|
811
|
+
if (panel === 'extensions' && !requireDesktopFeature('extensions', 'Extensions')) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
778
814
|
|
|
779
815
|
const nextBcfVisible = panel === 'bcf' ? !bcfPanelVisible : false;
|
|
780
816
|
const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
|
|
781
817
|
const nextLensVisible = panel === 'lens' ? !lensPanelVisible : false;
|
|
818
|
+
const nextExtensionsVisible = panel === 'extensions' ? !extensionsPanelVisible : false;
|
|
782
819
|
const isAddElementActive = activeTool === 'addElement';
|
|
783
820
|
const nextAddElementActive = panel === 'addElement' ? !isAddElementActive : false;
|
|
784
821
|
|
|
785
822
|
setBcfPanelVisible(nextBcfVisible);
|
|
786
823
|
setIdsPanelVisible(nextIdsVisible);
|
|
787
824
|
setLensPanelVisible(nextLensVisible);
|
|
825
|
+
setExtensionsPanelVisible(nextExtensionsVisible);
|
|
788
826
|
|
|
789
827
|
if (panel === 'addElement') {
|
|
790
828
|
setActiveTool(nextAddElementActive ? 'addElement' : 'select');
|
|
@@ -792,18 +830,20 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
792
830
|
setActiveTool('select');
|
|
793
831
|
}
|
|
794
832
|
|
|
795
|
-
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextAddElementActive) {
|
|
833
|
+
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
796
834
|
setRightPanelCollapsed(false);
|
|
797
835
|
}
|
|
798
836
|
}, [
|
|
799
837
|
activeAnalysisExtension?.placement,
|
|
800
838
|
activeTool,
|
|
801
839
|
bcfPanelVisible,
|
|
840
|
+
extensionsPanelVisible,
|
|
802
841
|
idsPanelVisible,
|
|
803
842
|
lensPanelVisible,
|
|
804
843
|
requireDesktopFeature,
|
|
805
844
|
setActiveTool,
|
|
806
845
|
setBcfPanelVisible,
|
|
846
|
+
setExtensionsPanelVisible,
|
|
807
847
|
setIdsPanelVisible,
|
|
808
848
|
setLensPanelVisible,
|
|
809
849
|
setRightPanelCollapsed,
|
|
@@ -836,6 +876,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
836
876
|
setBcfPanelVisible(false);
|
|
837
877
|
setIdsPanelVisible(false);
|
|
838
878
|
setLensPanelVisible(false);
|
|
879
|
+
setExtensionsPanelVisible(false);
|
|
839
880
|
// The right slot is single-tenant: when an analysis extension takes
|
|
840
881
|
// it over, the AddElement tool must release it too, otherwise its 3D
|
|
841
882
|
// click handler keeps placing elements behind the extension panel.
|
|
@@ -849,6 +890,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
849
890
|
analysisExtensionState.extensions,
|
|
850
891
|
setActiveTool,
|
|
851
892
|
setBcfPanelVisible,
|
|
893
|
+
setExtensionsPanelVisible,
|
|
852
894
|
setGanttPanelVisible,
|
|
853
895
|
setIdsPanelVisible,
|
|
854
896
|
setLensPanelVisible,
|
|
@@ -865,6 +907,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
865
907
|
if (bcfPanelVisible) panels.add('bcf');
|
|
866
908
|
if (idsPanelVisible) panels.add('ids');
|
|
867
909
|
if (lensPanelVisible) panels.add('lens');
|
|
910
|
+
if (extensionsPanelVisible) panels.add('extensions');
|
|
868
911
|
if (activeTool === 'addElement') panels.add('addElement');
|
|
869
912
|
if (analysisExtensionState.activeId) panels.add(analysisExtensionState.activeId);
|
|
870
913
|
return panels;
|
|
@@ -872,6 +915,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
872
915
|
activeTool,
|
|
873
916
|
analysisExtensionState.activeId,
|
|
874
917
|
bcfPanelVisible,
|
|
918
|
+
extensionsPanelVisible,
|
|
875
919
|
ganttPanelVisible,
|
|
876
920
|
idsPanelVisible,
|
|
877
921
|
lensPanelVisible,
|
|
@@ -888,6 +932,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
888
932
|
if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
|
|
889
933
|
if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
|
|
890
934
|
if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
|
|
935
|
+
if (activeWorkspacePanels.has('extensions')) return 'Extensions';
|
|
891
936
|
if (activeWorkspacePanels.has('addElement')) return 'Add Element';
|
|
892
937
|
return activeAnalysisExtension?.label ?? 'Analysis';
|
|
893
938
|
}, [activeAnalysisExtension?.label, activeWorkspacePanels]);
|
|
@@ -1192,6 +1237,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1192
1237
|
<TooltipContent>{workspacePanelLabel ? `Panels: ${workspacePanelLabel}` : 'Panels'}</TooltipContent>
|
|
1193
1238
|
</Tooltip>
|
|
1194
1239
|
<DropdownMenuContent align="start" className="w-56">
|
|
1240
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1241
|
+
Workspace
|
|
1242
|
+
</DropdownMenuLabel>
|
|
1195
1243
|
<DropdownMenuCheckboxItem
|
|
1196
1244
|
checked={activeWorkspacePanels.has('script')}
|
|
1197
1245
|
onCheckedChange={() => handleToggleBottomPanel('script')}
|
|
@@ -1214,6 +1262,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1214
1262
|
Schedule (Gantt)
|
|
1215
1263
|
</DropdownMenuCheckboxItem>
|
|
1216
1264
|
<DropdownMenuSeparator />
|
|
1265
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1266
|
+
Inspect & validate
|
|
1267
|
+
</DropdownMenuLabel>
|
|
1217
1268
|
<DropdownMenuCheckboxItem
|
|
1218
1269
|
checked={activeWorkspacePanels.has('bcf')}
|
|
1219
1270
|
onCheckedChange={() => handleToggleRightPanel('bcf')}
|
|
@@ -1235,6 +1286,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1235
1286
|
<Palette className="h-4 w-4 mr-2" />
|
|
1236
1287
|
Lens Rules
|
|
1237
1288
|
</DropdownMenuCheckboxItem>
|
|
1289
|
+
<DropdownMenuSeparator />
|
|
1290
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1291
|
+
Author
|
|
1292
|
+
</DropdownMenuLabel>
|
|
1238
1293
|
<DropdownMenuCheckboxItem
|
|
1239
1294
|
checked={activeWorkspacePanels.has('addElement')}
|
|
1240
1295
|
onCheckedChange={() => handleToggleRightPanel('addElement')}
|
|
@@ -1242,9 +1297,19 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1242
1297
|
<PackagePlus className="h-4 w-4 mr-2" />
|
|
1243
1298
|
Add Element
|
|
1244
1299
|
</DropdownMenuCheckboxItem>
|
|
1245
|
-
|
|
1300
|
+
<DropdownMenuCheckboxItem
|
|
1301
|
+
checked={activeWorkspacePanels.has('extensions')}
|
|
1302
|
+
onCheckedChange={() => handleToggleRightPanel('extensions')}
|
|
1303
|
+
>
|
|
1304
|
+
<Puzzle className="h-4 w-4 mr-2" />
|
|
1305
|
+
Extensions
|
|
1306
|
+
</DropdownMenuCheckboxItem>
|
|
1307
|
+
{(rightAnalysisExtensions.length > 0 || bottomAnalysisExtensions.length > 0) && (
|
|
1246
1308
|
<>
|
|
1247
1309
|
<DropdownMenuSeparator />
|
|
1310
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1311
|
+
Analysis extensions
|
|
1312
|
+
</DropdownMenuLabel>
|
|
1248
1313
|
{rightAnalysisExtensions.map((extension) => {
|
|
1249
1314
|
const Icon = extension.icon;
|
|
1250
1315
|
return (
|
|
@@ -1258,11 +1323,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1258
1323
|
</DropdownMenuCheckboxItem>
|
|
1259
1324
|
);
|
|
1260
1325
|
})}
|
|
1261
|
-
</>
|
|
1262
|
-
)}
|
|
1263
|
-
{bottomAnalysisExtensions.length > 0 && (
|
|
1264
|
-
<>
|
|
1265
|
-
<DropdownMenuSeparator />
|
|
1266
1326
|
{bottomAnalysisExtensions.map((extension) => {
|
|
1267
1327
|
const Icon = extension.icon;
|
|
1268
1328
|
return (
|
|
@@ -1414,17 +1474,42 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1414
1474
|
</TooltipContent>
|
|
1415
1475
|
</Tooltip>
|
|
1416
1476
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1477
|
+
{/*
|
|
1478
|
+
Selection action cluster — Hide / Frame / Isolate only make
|
|
1479
|
+
sense with a selection, so they don't get to live in the
|
|
1480
|
+
toolbar chrome at rest. When a user selects anything, the
|
|
1481
|
+
slot opens with a "N selected" pill + the three actions next
|
|
1482
|
+
to it. Hotkeys (Del / F / I / =) keep working regardless of
|
|
1483
|
+
whether the chip is rendered, so power users feel no change.
|
|
1484
|
+
|
|
1485
|
+
The chip lives in the same separator zone the buttons used to
|
|
1486
|
+
occupy so the spatial location is familiar to muscle memory.
|
|
1487
|
+
*/}
|
|
1488
|
+
{selectionCount > 0 && (
|
|
1489
|
+
<div
|
|
1490
|
+
className="flex items-center gap-0.5 pl-1.5 pr-0.5 rounded-md border border-primary/30 bg-primary/5 transition-opacity duration-150"
|
|
1491
|
+
role="group"
|
|
1492
|
+
aria-label={`Selection actions — ${selectionCount} selected`}
|
|
1493
|
+
>
|
|
1494
|
+
<span
|
|
1495
|
+
className="text-[10px] font-semibold tabular-nums text-primary uppercase tracking-wide whitespace-nowrap pr-1.5"
|
|
1496
|
+
aria-hidden="true"
|
|
1497
|
+
>
|
|
1498
|
+
{selectionCount} sel
|
|
1499
|
+
</span>
|
|
1500
|
+
<ActionButton icon={Equal} label="Isolate Selection (Set Basket)" onClick={handleIsolate} shortcut="I / =" />
|
|
1501
|
+
<ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del / Space" />
|
|
1502
|
+
<ActionButton
|
|
1503
|
+
icon={Crosshair}
|
|
1504
|
+
label="Frame Selection"
|
|
1505
|
+
onClick={() => cameraCallbacks.frameSelection?.()}
|
|
1506
|
+
shortcut="F"
|
|
1507
|
+
/>
|
|
1508
|
+
</div>
|
|
1509
|
+
)}
|
|
1510
|
+
|
|
1419
1511
|
<ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
|
|
1420
1512
|
<ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
|
|
1421
|
-
<ActionButton
|
|
1422
|
-
icon={Crosshair}
|
|
1423
|
-
label="Frame Selection"
|
|
1424
|
-
onClick={() => cameraCallbacks.frameSelection?.()}
|
|
1425
|
-
shortcut="F"
|
|
1426
|
-
disabled={!hasSelection}
|
|
1427
|
-
/>
|
|
1428
1513
|
|
|
1429
1514
|
<DropdownMenu>
|
|
1430
1515
|
<Tooltip>
|
|
@@ -1441,7 +1526,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1441
1526
|
aria-label={mergeLayers ? 'Class Visibility (Merge Multilayer Walls is on)' : 'Class Visibility'}
|
|
1442
1527
|
className="relative"
|
|
1443
1528
|
>
|
|
1444
|
-
<
|
|
1529
|
+
<Filter className="h-4 w-4" />
|
|
1445
1530
|
{mergeLayers && (
|
|
1446
1531
|
// Tiny accent dot announcing that a non-default load
|
|
1447
1532
|
// setting is active. Decorative — semantics live on
|
|
@@ -1486,6 +1571,15 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1486
1571
|
Show Site
|
|
1487
1572
|
</DropdownMenuCheckboxItem>
|
|
1488
1573
|
)}
|
|
1574
|
+
{hasIfcAnnotations && (
|
|
1575
|
+
<DropdownMenuCheckboxItem
|
|
1576
|
+
checked={typeVisibility.ifcAnnotations}
|
|
1577
|
+
onCheckedChange={() => toggleTypeVisibility('ifcAnnotations')}
|
|
1578
|
+
>
|
|
1579
|
+
<Pencil className="h-4 w-4 mr-2" style={{ color: '#e4b400' }} />
|
|
1580
|
+
Show Annotations & Grids
|
|
1581
|
+
</DropdownMenuCheckboxItem>
|
|
1582
|
+
)}
|
|
1489
1583
|
|
|
1490
1584
|
{/* Load-time toggles live below the runtime visibility
|
|
1491
1585
|
switches — they apply on next model open rather than
|
|
@@ -1518,29 +1612,15 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1518
1612
|
{/* ── Camera & View ── */}
|
|
1519
1613
|
<ActionButton icon={Home} label="Home (Isometric + Reset Visibility)" onClick={handleHome} shortcut="H" />
|
|
1520
1614
|
|
|
1521
|
-
{/*
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
(e.currentTarget as HTMLButtonElement).blur();
|
|
1529
|
-
toggleProjectionMode();
|
|
1530
|
-
}}
|
|
1531
|
-
className={cn(projectionMode === 'orthographic' && 'bg-primary text-primary-foreground')}
|
|
1532
|
-
>
|
|
1533
|
-
<Orbit className="h-4 w-4" />
|
|
1534
|
-
</Button>
|
|
1535
|
-
</TooltipTrigger>
|
|
1536
|
-
<TooltipContent>
|
|
1537
|
-
{projectionMode === 'orthographic' ? 'Switch to Perspective' : 'Switch to Orthographic'}
|
|
1538
|
-
</TooltipContent>
|
|
1539
|
-
</Tooltip>
|
|
1540
|
-
|
|
1541
|
-
{/* Cesium 3D Context toggle — web only, only when model has georeferencing */}
|
|
1615
|
+
{/*
|
|
1616
|
+
Cesium 3D World Context — sits next to Home as a raw button so
|
|
1617
|
+
the world-context affordance is one click away when a model has
|
|
1618
|
+
georeferencing. When active, the "Move georeference" sub-toggle
|
|
1619
|
+
appears beside it (its amber tint signals a modal pose whose
|
|
1620
|
+
exit affordance must stay visible).
|
|
1621
|
+
*/}
|
|
1542
1622
|
{cesiumAvailable && !desktopShell && (
|
|
1543
|
-
|
|
1623
|
+
<>
|
|
1544
1624
|
<Tooltip>
|
|
1545
1625
|
<TooltipTrigger asChild>
|
|
1546
1626
|
<Button
|
|
@@ -1589,46 +1669,40 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1589
1669
|
</TooltipContent>
|
|
1590
1670
|
</Tooltip>
|
|
1591
1671
|
)}
|
|
1592
|
-
|
|
1672
|
+
</>
|
|
1593
1673
|
)}
|
|
1594
1674
|
|
|
1595
|
-
{/*
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
toggleHoverTooltips();
|
|
1604
|
-
}}
|
|
1605
|
-
className={cn(hoverTooltipsEnabled && 'bg-primary text-primary-foreground')}
|
|
1606
|
-
>
|
|
1607
|
-
<Info className="h-4 w-4" />
|
|
1608
|
-
</Button>
|
|
1609
|
-
</TooltipTrigger>
|
|
1610
|
-
<TooltipContent>
|
|
1611
|
-
{hoverTooltipsEnabled ? 'Disable' : 'Enable'} Hover Tooltips
|
|
1612
|
-
</TooltipContent>
|
|
1613
|
-
</Tooltip>
|
|
1614
|
-
|
|
1615
|
-
{/* Preset Views dropdown */}
|
|
1675
|
+
{/*
|
|
1676
|
+
Consolidated View dropdown — holds projection toggle, preset
|
|
1677
|
+
views, and hover tooltips. These are "view options" the user
|
|
1678
|
+
reaches for occasionally, and rendering each as a raw icon
|
|
1679
|
+
button used to dominate the toolbar's right half. Cesium stayed
|
|
1680
|
+
inline (above) because the world-context overlay is a primary
|
|
1681
|
+
affordance, not a tucked-away view setting.
|
|
1682
|
+
*/}
|
|
1616
1683
|
<DropdownMenu>
|
|
1617
1684
|
<Tooltip>
|
|
1618
1685
|
<TooltipTrigger asChild>
|
|
1619
1686
|
<DropdownMenuTrigger asChild>
|
|
1620
|
-
<Button
|
|
1687
|
+
<Button
|
|
1688
|
+
variant={(projectionMode === 'orthographic' || hoverTooltipsEnabled) ? 'default' : 'ghost'}
|
|
1689
|
+
size="icon-sm"
|
|
1690
|
+
aria-label="View options"
|
|
1691
|
+
className={cn((projectionMode === 'orthographic' || hoverTooltipsEnabled) && 'bg-primary text-primary-foreground')}
|
|
1692
|
+
>
|
|
1621
1693
|
<Grid3x3 className="h-4 w-4" />
|
|
1622
1694
|
</Button>
|
|
1623
1695
|
</DropdownMenuTrigger>
|
|
1624
1696
|
</TooltipTrigger>
|
|
1625
|
-
<TooltipContent>
|
|
1697
|
+
<TooltipContent>View options</TooltipContent>
|
|
1626
1698
|
</Tooltip>
|
|
1627
|
-
<DropdownMenuContent>
|
|
1699
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
1700
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1701
|
+
Preset views
|
|
1702
|
+
</DropdownMenuLabel>
|
|
1628
1703
|
<DropdownMenuItem onClick={handleHome}>
|
|
1629
1704
|
<Box className="h-4 w-4 mr-2" /> Isometric <span className="ml-auto text-xs opacity-60">H</span>
|
|
1630
1705
|
</DropdownMenuItem>
|
|
1631
|
-
<DropdownMenuSeparator />
|
|
1632
1706
|
<DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('top')}>
|
|
1633
1707
|
<ArrowUp className="h-4 w-4 mr-2" /> Top <span className="ml-auto text-xs opacity-60">1</span>
|
|
1634
1708
|
</DropdownMenuItem>
|
|
@@ -1647,12 +1721,37 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1647
1721
|
<DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('right')}>
|
|
1648
1722
|
<ArrowRight className="h-4 w-4 mr-2" /> Right <span className="ml-auto text-xs opacity-60">6</span>
|
|
1649
1723
|
</DropdownMenuItem>
|
|
1724
|
+
<DropdownMenuSeparator />
|
|
1725
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1726
|
+
Projection
|
|
1727
|
+
</DropdownMenuLabel>
|
|
1728
|
+
<DropdownMenuCheckboxItem
|
|
1729
|
+
checked={projectionMode === 'orthographic'}
|
|
1730
|
+
onCheckedChange={() => toggleProjectionMode()}
|
|
1731
|
+
>
|
|
1732
|
+
<Orbit className="h-4 w-4 mr-2" />
|
|
1733
|
+
Orthographic
|
|
1734
|
+
</DropdownMenuCheckboxItem>
|
|
1735
|
+
<DropdownMenuSeparator />
|
|
1736
|
+
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1737
|
+
Helpers
|
|
1738
|
+
</DropdownMenuLabel>
|
|
1739
|
+
<DropdownMenuCheckboxItem
|
|
1740
|
+
checked={hoverTooltipsEnabled}
|
|
1741
|
+
onCheckedChange={() => toggleHoverTooltips()}
|
|
1742
|
+
>
|
|
1743
|
+
<Info className="h-4 w-4 mr-2" />
|
|
1744
|
+
Hover tooltips
|
|
1745
|
+
</DropdownMenuCheckboxItem>
|
|
1650
1746
|
</DropdownMenuContent>
|
|
1651
1747
|
</DropdownMenu>
|
|
1652
1748
|
|
|
1653
1749
|
{/* Spacer */}
|
|
1654
1750
|
<div className="flex-1" />
|
|
1655
1751
|
|
|
1752
|
+
{/* Extension toolbar contributions (right-aligned) */}
|
|
1753
|
+
<ExtensionToolbarSlot slot="toolbar.right" />
|
|
1754
|
+
|
|
1656
1755
|
{/* Loading Progress */}
|
|
1657
1756
|
{loading && (geometryProgress || metadataProgress || progress) && (
|
|
1658
1757
|
<div className="flex items-center gap-2 mr-4">
|
|
@@ -1678,26 +1777,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1678
1777
|
<span className="text-xs text-destructive mr-4">{error}</span>
|
|
1679
1778
|
)}
|
|
1680
1779
|
|
|
1681
|
-
{/* Right Side Actions
|
|
1780
|
+
{/* Right Side Actions — /mcp moved to the Info dialog header so
|
|
1781
|
+
the toolbar's meta cluster stays focused on shell chrome
|
|
1782
|
+
(Settings · Theme · Help). */}
|
|
1682
1783
|
<div className="flex items-center gap-2 ml-2 pl-2 border-l border-zinc-200 dark:border-zinc-700/60">
|
|
1683
|
-
{/* /mcp cross-link — lives in the meta cluster (Settings / Theme /
|
|
1684
|
-
Help) so it shares space with shell-level navigation rather
|
|
1685
|
-
than competing with the modeling tools to its left. */}
|
|
1686
|
-
<Tooltip>
|
|
1687
|
-
<TooltipTrigger asChild>
|
|
1688
|
-
<Button
|
|
1689
|
-
variant="ghost"
|
|
1690
|
-
size="icon"
|
|
1691
|
-
className="rounded-full"
|
|
1692
|
-
onClick={() => navigateToPath('/mcp')}
|
|
1693
|
-
aria-label="Open ifc-lite MCP"
|
|
1694
|
-
>
|
|
1695
|
-
<Sparkles className="!h-[20px] !w-[20px]" />
|
|
1696
|
-
</Button>
|
|
1697
|
-
</TooltipTrigger>
|
|
1698
|
-
<TooltipContent>Drive ifc-lite from any LLM (MCP)</TooltipContent>
|
|
1699
|
-
</Tooltip>
|
|
1700
|
-
|
|
1701
1784
|
{desktopShell ? (
|
|
1702
1785
|
<Tooltip>
|
|
1703
1786
|
<TooltipTrigger asChild>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
PanelRightOpen,
|
|
30
30
|
Undo2,
|
|
31
31
|
Redo2,
|
|
32
|
+
Wrench,
|
|
32
33
|
} from 'lucide-react';
|
|
33
34
|
import { Button } from '@/components/ui/button';
|
|
34
35
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -57,6 +58,8 @@ import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
|
|
|
57
58
|
import { navigateToPath } from '@/services/app-navigation';
|
|
58
59
|
import { CodeEditor } from './CodeEditor';
|
|
59
60
|
import { ChatPanel } from './ChatPanel';
|
|
61
|
+
import { PromoteToolDialog } from '@/components/extensions/PromoteToolDialog';
|
|
62
|
+
import { useOptionalExtensionHost } from '@/sdk/ExtensionHostProvider';
|
|
60
63
|
import type { LogEntry } from '@/store/slices/scriptSlice';
|
|
61
64
|
|
|
62
65
|
interface ScriptPanelProps {
|
|
@@ -87,6 +90,8 @@ function useScriptState() {
|
|
|
87
90
|
const undoScriptEditor = useViewerStore((s) => s.undoScriptEditor);
|
|
88
91
|
const redoScriptEditor = useViewerStore((s) => s.redoScriptEditor);
|
|
89
92
|
const queueChatRepairRequest = useViewerStore((s) => s.queueChatRepairRequest);
|
|
93
|
+
const chatToolReady = useViewerStore((s) => s.chatToolReady);
|
|
94
|
+
const setChatToolReady = useViewerStore((s) => s.setChatToolReady);
|
|
90
95
|
|
|
91
96
|
return {
|
|
92
97
|
editorContent,
|
|
@@ -111,6 +116,8 @@ function useScriptState() {
|
|
|
111
116
|
undoScriptEditor,
|
|
112
117
|
redoScriptEditor,
|
|
113
118
|
queueChatRepairRequest,
|
|
119
|
+
chatToolReady,
|
|
120
|
+
setChatToolReady,
|
|
114
121
|
};
|
|
115
122
|
}
|
|
116
123
|
|
|
@@ -138,9 +145,12 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
138
145
|
undoScriptEditor,
|
|
139
146
|
redoScriptEditor,
|
|
140
147
|
queueChatRepairRequest,
|
|
148
|
+
chatToolReady,
|
|
149
|
+
setChatToolReady,
|
|
141
150
|
} = useScriptState();
|
|
142
151
|
|
|
143
152
|
const { execute, reset } = useSandbox();
|
|
153
|
+
const extensionHost = useOptionalExtensionHost();
|
|
144
154
|
const [outputCollapsed, setOutputCollapsed] = useState(false);
|
|
145
155
|
const chatPanelVisible = useViewerStore((s) => s.chatPanelVisible);
|
|
146
156
|
const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible);
|
|
@@ -218,8 +228,13 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
218
228
|
|
|
219
229
|
const handleRun = useCallback(async () => {
|
|
220
230
|
if (executionState === 'running') return;
|
|
231
|
+
const startedAt = performance.now();
|
|
221
232
|
await execute(editorContent);
|
|
222
|
-
|
|
233
|
+
extensionHost?.emitAction('script.execute', {
|
|
234
|
+
templateId: activeScriptId ?? undefined,
|
|
235
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
236
|
+
});
|
|
237
|
+
}, [execute, editorContent, executionState, extensionHost, activeScriptId]);
|
|
223
238
|
|
|
224
239
|
const handleSave = useCallback(() => {
|
|
225
240
|
if (activeScriptId) {
|
|
@@ -233,6 +248,9 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
233
248
|
createScript(name, code);
|
|
234
249
|
}, [createScript]);
|
|
235
250
|
|
|
251
|
+
const [promoteOpen, setPromoteOpen] = useState(false);
|
|
252
|
+
const canPromote = !!extensionHost && editorContent.trim().length > 0;
|
|
253
|
+
|
|
236
254
|
const handleDeleteConfirm = useCallback(() => {
|
|
237
255
|
if (deleteConfirmId) {
|
|
238
256
|
deleteScript(deleteConfirmId);
|
|
@@ -330,6 +348,47 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
330
348
|
)}
|
|
331
349
|
</div>
|
|
332
350
|
|
|
351
|
+
{/* Post-authoring "install as tool" banner — surfaces right
|
|
352
|
+
where the AI-written code lands so the user never has to
|
|
353
|
+
hunt for the Promote button. Highlighted (accent fill +
|
|
354
|
+
ring) so the install step reads as the obvious next move,
|
|
355
|
+
not a faint afterthought. */}
|
|
356
|
+
{chatToolReady?.kind === 'script' && (
|
|
357
|
+
<div className="shrink-0 border-b bg-primary/15 px-3 py-2.5 ring-1 ring-inset ring-primary/40">
|
|
358
|
+
<div className="flex items-center gap-2.5">
|
|
359
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
|
360
|
+
<Wrench className="h-4 w-4" />
|
|
361
|
+
</div>
|
|
362
|
+
<div className="flex-1 min-w-0">
|
|
363
|
+
<div className="text-xs font-semibold">This script is ready</div>
|
|
364
|
+
<div className="text-[11px] text-muted-foreground">
|
|
365
|
+
Install it as a one-click button in your toolbar.
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
<Button
|
|
369
|
+
size="sm"
|
|
370
|
+
onClick={() => {
|
|
371
|
+
setPromoteOpen(true);
|
|
372
|
+
setChatToolReady(null);
|
|
373
|
+
}}
|
|
374
|
+
className="shrink-0"
|
|
375
|
+
>
|
|
376
|
+
<Wrench className="mr-1 h-3.5 w-3.5" />
|
|
377
|
+
Install as tool
|
|
378
|
+
</Button>
|
|
379
|
+
<Button
|
|
380
|
+
size="icon-xs"
|
|
381
|
+
variant="ghost"
|
|
382
|
+
onClick={() => setChatToolReady(null)}
|
|
383
|
+
aria-label="Dismiss"
|
|
384
|
+
className="shrink-0"
|
|
385
|
+
>
|
|
386
|
+
<X className="h-3.5 w-3.5" />
|
|
387
|
+
</Button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
|
|
333
392
|
{/* Toolbar */}
|
|
334
393
|
<div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
|
|
335
394
|
<Tooltip>
|
|
@@ -357,6 +416,29 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
357
416
|
<TooltipContent>Save (Ctrl+S)</TooltipContent>
|
|
358
417
|
</Tooltip>
|
|
359
418
|
|
|
419
|
+
{/* Save-as-tool — the explicit, always-visible bridge from a
|
|
420
|
+
one-shot script to a persistent toolbar button. A labelled
|
|
421
|
+
outline button (not a buried icon) so the "keep this"
|
|
422
|
+
step is discoverable without nagging. */}
|
|
423
|
+
<Tooltip>
|
|
424
|
+
<TooltipTrigger asChild>
|
|
425
|
+
<Button
|
|
426
|
+
variant="outline"
|
|
427
|
+
size="sm"
|
|
428
|
+
onClick={() => setPromoteOpen(true)}
|
|
429
|
+
disabled={!canPromote}
|
|
430
|
+
aria-label="Save this script as a persistent tool"
|
|
431
|
+
className="gap-1"
|
|
432
|
+
>
|
|
433
|
+
<Wrench className="h-3.5 w-3.5" />
|
|
434
|
+
Save as tool
|
|
435
|
+
</Button>
|
|
436
|
+
</TooltipTrigger>
|
|
437
|
+
<TooltipContent>
|
|
438
|
+
Turn this script into a permanent one-click button in your toolbar
|
|
439
|
+
</TooltipContent>
|
|
440
|
+
</Tooltip>
|
|
441
|
+
|
|
360
442
|
<Tooltip>
|
|
361
443
|
<TooltipTrigger asChild>
|
|
362
444
|
<Button
|
|
@@ -479,6 +561,16 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
479
561
|
<AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
|
|
480
562
|
<div className="min-w-0">
|
|
481
563
|
<span className="whitespace-pre-wrap break-all">{lastError}</span>
|
|
564
|
+
{/* Sandbox-globals hint — when the error names a
|
|
565
|
+
browser-context API the sandbox doesn't expose,
|
|
566
|
+
surface a one-line cue so the user understands
|
|
567
|
+
why the rewrite is needed before clicking Fix. */}
|
|
568
|
+
{/(document|window|navigator|location|fetch|XMLHttpRequest|localStorage|indexedDB|setTimeout|setInterval) is not defined/.test(lastError) && (
|
|
569
|
+
<div className="mt-1 text-[11px] text-muted-foreground font-sans">
|
|
570
|
+
Scripts run in a QuickJS sandbox — no DOM, no <code className="font-mono">fetch</code>, no browser globals.
|
|
571
|
+
Use <code className="font-mono">bim.*</code> APIs for viewer / data / export side-effects.
|
|
572
|
+
</div>
|
|
573
|
+
)}
|
|
482
574
|
<div className="mt-1">
|
|
483
575
|
<Button
|
|
484
576
|
variant="outline"
|
|
@@ -555,6 +647,18 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
555
647
|
</DialogFooter>
|
|
556
648
|
</DialogContent>
|
|
557
649
|
</Dialog>
|
|
650
|
+
|
|
651
|
+
{promoteOpen && extensionHost && (
|
|
652
|
+
<PromoteToolDialog
|
|
653
|
+
open={promoteOpen}
|
|
654
|
+
source={editorContent}
|
|
655
|
+
initialName={
|
|
656
|
+
savedScripts.find((s) => s.id === activeScriptId)?.name
|
|
657
|
+
?? 'My tool'
|
|
658
|
+
}
|
|
659
|
+
onClose={() => setPromoteOpen(false)}
|
|
660
|
+
/>
|
|
661
|
+
)}
|
|
558
662
|
</div>
|
|
559
663
|
);
|
|
560
664
|
}
|