@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
|
@@ -0,0 +1,388 @@
|
|
|
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
|
+
* SearchModal.text — the Search tab content.
|
|
7
|
+
*
|
|
8
|
+
* Chip filters (field match type + per-model include) narrow the pool
|
|
9
|
+
* of results provided by the parent modal, which owns the scan + merge.
|
|
10
|
+
* The result list is virtualized via @tanstack/react-virtual so 5000
|
|
11
|
+
* rows render at constant cost. Batch actions route through the
|
|
12
|
+
* existing selection / visibility slices.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
16
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
17
|
+
import { Crosshair, SquareX, ListChecks, Filter } from 'lucide-react';
|
|
18
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
19
|
+
import { useViewerStore } from '@/store';
|
|
20
|
+
import { toGlobalIdFromModels } from '@/store/globalId';
|
|
21
|
+
import { cn } from '@/lib/utils';
|
|
22
|
+
import { Button } from '@/components/ui/button';
|
|
23
|
+
import { Badge } from '@/components/ui/badge';
|
|
24
|
+
import type { SearchResult } from '@/lib/search/tier0-scan';
|
|
25
|
+
import type { SearchFieldFilter } from '@/store/slices/searchSlice';
|
|
26
|
+
|
|
27
|
+
const ROW_HEIGHT = 36;
|
|
28
|
+
|
|
29
|
+
const FIELD_FILTERS: { value: SearchFieldFilter; label: string }[] = [
|
|
30
|
+
{ value: 'all', label: 'All' },
|
|
31
|
+
{ value: 'name', label: 'Name' },
|
|
32
|
+
{ value: 'type', label: 'Type' },
|
|
33
|
+
{ value: 'globalId', label: 'GUID' },
|
|
34
|
+
{ value: 'description', label: 'Description' },
|
|
35
|
+
{ value: 'objectType', label: 'ObjectType' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export interface SearchModalTextProps {
|
|
39
|
+
/** Full result pool from the parent modal (before filter chips). */
|
|
40
|
+
results: SearchResult[];
|
|
41
|
+
/** All modelIds currently loaded (for the model-filter chips). */
|
|
42
|
+
availableModelIds: readonly string[];
|
|
43
|
+
/** Close the parent modal — invoked on Enter-commit from a row. */
|
|
44
|
+
onClose: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function SearchModalText({ results, availableModelIds, onClose }: SearchModalTextProps) {
|
|
48
|
+
const {
|
|
49
|
+
searchFieldFilter,
|
|
50
|
+
searchModelFilter,
|
|
51
|
+
searchQuery,
|
|
52
|
+
searchHighlightIndex,
|
|
53
|
+
selectedEntitiesSet,
|
|
54
|
+
models,
|
|
55
|
+
setSearchFieldFilter,
|
|
56
|
+
toggleSearchModelFilter,
|
|
57
|
+
clearSearchModelFilter,
|
|
58
|
+
setSearchHighlightIndex,
|
|
59
|
+
setSelectedEntity,
|
|
60
|
+
setSelectedEntityId,
|
|
61
|
+
addEntitiesToSelection,
|
|
62
|
+
toggleEntitySelection,
|
|
63
|
+
clearEntitySelection,
|
|
64
|
+
enterVimCycle,
|
|
65
|
+
cameraCallbacks,
|
|
66
|
+
} = useViewerStore(
|
|
67
|
+
useShallow((s) => ({
|
|
68
|
+
searchFieldFilter: s.searchFieldFilter,
|
|
69
|
+
searchModelFilter: s.searchModelFilter,
|
|
70
|
+
searchQuery: s.searchQuery,
|
|
71
|
+
searchHighlightIndex: s.searchHighlightIndex,
|
|
72
|
+
selectedEntitiesSet: s.selectedEntitiesSet,
|
|
73
|
+
models: s.models,
|
|
74
|
+
setSearchFieldFilter: s.setSearchFieldFilter,
|
|
75
|
+
toggleSearchModelFilter: s.toggleSearchModelFilter,
|
|
76
|
+
clearSearchModelFilter: s.clearSearchModelFilter,
|
|
77
|
+
setSearchHighlightIndex: s.setSearchHighlightIndex,
|
|
78
|
+
setSelectedEntity: s.setSelectedEntity,
|
|
79
|
+
setSelectedEntityId: s.setSelectedEntityId,
|
|
80
|
+
addEntitiesToSelection: s.addEntitiesToSelection,
|
|
81
|
+
toggleEntitySelection: s.toggleEntitySelection,
|
|
82
|
+
clearEntitySelection: s.clearEntitySelection,
|
|
83
|
+
enterVimCycle: s.enterVimCycle,
|
|
84
|
+
cameraCallbacks: s.cameraCallbacks,
|
|
85
|
+
})),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Filter the result pool by the active chip selections.
|
|
89
|
+
const filtered = useMemo(() => {
|
|
90
|
+
return results.filter((r) => {
|
|
91
|
+
if (searchFieldFilter !== 'all' && r.matchField !== searchFieldFilter) return false;
|
|
92
|
+
if (searchModelFilter && !searchModelFilter.has(r.modelId)) return false;
|
|
93
|
+
return true;
|
|
94
|
+
});
|
|
95
|
+
}, [results, searchFieldFilter, searchModelFilter]);
|
|
96
|
+
|
|
97
|
+
// Virtualized list setup. `count` tracks filtered.length; scrollElement
|
|
98
|
+
// is the parent div the virtualizer measures.
|
|
99
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
100
|
+
const virtualizer = useVirtualizer({
|
|
101
|
+
count: filtered.length,
|
|
102
|
+
getScrollElement: () => scrollRef.current,
|
|
103
|
+
estimateSize: () => ROW_HEIGHT,
|
|
104
|
+
overscan: 10,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Keep the highlight index in range as filtered results change.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (filtered.length === 0) {
|
|
110
|
+
if (searchHighlightIndex !== 0) setSearchHighlightIndex(0);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (searchHighlightIndex >= filtered.length) {
|
|
114
|
+
setSearchHighlightIndex(Math.max(0, filtered.length - 1));
|
|
115
|
+
}
|
|
116
|
+
}, [filtered, searchHighlightIndex, setSearchHighlightIndex]);
|
|
117
|
+
|
|
118
|
+
// Scroll the highlighted row into view when it moves.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (filtered.length === 0) return;
|
|
121
|
+
virtualizer.scrollToIndex(searchHighlightIndex, { align: 'auto' });
|
|
122
|
+
}, [searchHighlightIndex, filtered.length, virtualizer]);
|
|
123
|
+
|
|
124
|
+
/** Primary "click row" handler — selects + frames + enters vim cycle. */
|
|
125
|
+
const commit = useCallback(
|
|
126
|
+
(r: SearchResult, indexInFiltered: number) => {
|
|
127
|
+
const ref = { modelId: r.modelId, expressId: r.expressId };
|
|
128
|
+
const isLegacy = r.modelId === 'legacy' || r.modelId === '__legacy__' || models.size === 0;
|
|
129
|
+
const globalId = isLegacy ? r.expressId : toGlobalIdFromModels(models, r.modelId, r.expressId);
|
|
130
|
+
setSelectedEntityId(globalId);
|
|
131
|
+
setSelectedEntity(ref);
|
|
132
|
+
if (cameraCallbacks.frameSelection) {
|
|
133
|
+
window.setTimeout(() => cameraCallbacks.frameSelection?.(), 50);
|
|
134
|
+
}
|
|
135
|
+
enterVimCycle(searchQuery, filtered, indexInFiltered);
|
|
136
|
+
onClose();
|
|
137
|
+
},
|
|
138
|
+
[
|
|
139
|
+
cameraCallbacks,
|
|
140
|
+
enterVimCycle,
|
|
141
|
+
filtered,
|
|
142
|
+
models,
|
|
143
|
+
onClose,
|
|
144
|
+
searchQuery,
|
|
145
|
+
setSelectedEntity,
|
|
146
|
+
setSelectedEntityId,
|
|
147
|
+
],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Additive toggle — adds OR removes from multi-selection without
|
|
152
|
+
* closing. Uses `toggleEntitySelection` so a second Shift+Enter (or
|
|
153
|
+
* a second checkbox click on the same row) deselects, rather than
|
|
154
|
+
* being a no-op that forces the user to clear the entire selection.
|
|
155
|
+
*/
|
|
156
|
+
const toggleAdditive = useCallback(
|
|
157
|
+
(r: SearchResult) => {
|
|
158
|
+
toggleEntitySelection({ modelId: r.modelId, expressId: r.expressId });
|
|
159
|
+
},
|
|
160
|
+
[toggleEntitySelection],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Batch: add every filtered result to multi-selection in a single
|
|
165
|
+
* Zustand `set`. The naïve loop over `addEntityToSelection` triggered
|
|
166
|
+
* one re-render per row — visibly janky on a 5K-row filtered set.
|
|
167
|
+
*/
|
|
168
|
+
const selectAll = useCallback(() => {
|
|
169
|
+
if (filtered.length === 0) return;
|
|
170
|
+
addEntitiesToSelection(
|
|
171
|
+
filtered.map((r) => ({ modelId: r.modelId, expressId: r.expressId })),
|
|
172
|
+
);
|
|
173
|
+
}, [addEntitiesToSelection, filtered]);
|
|
174
|
+
|
|
175
|
+
/** Batch: frame whatever is the primary selection (if any). */
|
|
176
|
+
const frame = useCallback(() => {
|
|
177
|
+
cameraCallbacks.frameSelection?.();
|
|
178
|
+
}, [cameraCallbacks]);
|
|
179
|
+
|
|
180
|
+
const multiCount = selectedEntitiesSet.size;
|
|
181
|
+
const hasModelChips = availableModelIds.length > 1;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className="flex flex-1 min-h-0 flex-col">
|
|
185
|
+
{/* ── Chip filters ── */}
|
|
186
|
+
<div className="flex flex-wrap items-center gap-1.5 border-b px-4 py-2 text-xs">
|
|
187
|
+
<Filter className="h-3 w-3 text-muted-foreground" />
|
|
188
|
+
<span className="mr-1 text-muted-foreground">Field:</span>
|
|
189
|
+
{FIELD_FILTERS.map((f) => (
|
|
190
|
+
<button
|
|
191
|
+
key={f.value}
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => setSearchFieldFilter(f.value)}
|
|
194
|
+
className={cn(
|
|
195
|
+
'rounded border px-2 py-0.5 transition-colors',
|
|
196
|
+
searchFieldFilter === f.value
|
|
197
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
198
|
+
: 'border-zinc-300 hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800',
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
{f.label}
|
|
202
|
+
</button>
|
|
203
|
+
))}
|
|
204
|
+
{hasModelChips && (
|
|
205
|
+
<>
|
|
206
|
+
<span className="mx-2 h-4 w-px bg-zinc-300 dark:bg-zinc-700" />
|
|
207
|
+
<span className="mr-1 text-muted-foreground">Models:</span>
|
|
208
|
+
{availableModelIds.map((id) => {
|
|
209
|
+
const included = searchModelFilter === null || searchModelFilter.has(id);
|
|
210
|
+
const model = models.get(id);
|
|
211
|
+
const label = model?.name ?? id.slice(0, 6);
|
|
212
|
+
return (
|
|
213
|
+
<button
|
|
214
|
+
key={id}
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={() => toggleSearchModelFilter(id, availableModelIds)}
|
|
217
|
+
className={cn(
|
|
218
|
+
'rounded border px-2 py-0.5 transition-colors',
|
|
219
|
+
included
|
|
220
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
221
|
+
: 'border-zinc-300 text-muted-foreground line-through hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800',
|
|
222
|
+
)}
|
|
223
|
+
title={id}
|
|
224
|
+
>
|
|
225
|
+
{label}
|
|
226
|
+
</button>
|
|
227
|
+
);
|
|
228
|
+
})}
|
|
229
|
+
{searchModelFilter !== null && (
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={clearSearchModelFilter}
|
|
233
|
+
className="ml-1 text-[10px] text-muted-foreground underline hover:text-foreground"
|
|
234
|
+
>
|
|
235
|
+
reset
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* ── Virtualized results list ── */}
|
|
243
|
+
<div
|
|
244
|
+
ref={scrollRef}
|
|
245
|
+
className="flex-1 min-h-0 overflow-y-auto"
|
|
246
|
+
role="listbox"
|
|
247
|
+
aria-label="Search results"
|
|
248
|
+
tabIndex={0}
|
|
249
|
+
onKeyDown={(e) => {
|
|
250
|
+
if (filtered.length === 0) return;
|
|
251
|
+
if (e.key === 'ArrowDown') {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
const next = (searchHighlightIndex + 1) % filtered.length;
|
|
254
|
+
setSearchHighlightIndex(next);
|
|
255
|
+
} else if (e.key === 'ArrowUp') {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
const next = (searchHighlightIndex - 1 + filtered.length) % filtered.length;
|
|
258
|
+
setSearchHighlightIndex(next);
|
|
259
|
+
} else if (e.key === 'Enter') {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
const target = filtered[searchHighlightIndex];
|
|
262
|
+
if (target) {
|
|
263
|
+
if (e.shiftKey) toggleAdditive(target);
|
|
264
|
+
else commit(target, searchHighlightIndex);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
{filtered.length === 0 ? (
|
|
270
|
+
<div className="px-4 py-8 text-center text-xs text-muted-foreground">
|
|
271
|
+
{results.length === 0
|
|
272
|
+
? 'Start typing to search — GlobalIds, names, IFC types, descriptions.'
|
|
273
|
+
: 'No results match the active filters. Clear chips to widen the search.'}
|
|
274
|
+
</div>
|
|
275
|
+
) : (
|
|
276
|
+
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
|
|
277
|
+
{virtualizer.getVirtualItems().map((vRow) => {
|
|
278
|
+
const r = filtered[vRow.index];
|
|
279
|
+
const key = `${r.modelId}:${r.expressId}`;
|
|
280
|
+
const isChecked = selectedEntitiesSet.has(key);
|
|
281
|
+
const isHighlighted = vRow.index === searchHighlightIndex;
|
|
282
|
+
return (
|
|
283
|
+
<div
|
|
284
|
+
key={key}
|
|
285
|
+
role="option"
|
|
286
|
+
aria-selected={isHighlighted}
|
|
287
|
+
style={{
|
|
288
|
+
position: 'absolute',
|
|
289
|
+
top: 0,
|
|
290
|
+
left: 0,
|
|
291
|
+
width: '100%',
|
|
292
|
+
height: vRow.size,
|
|
293
|
+
transform: `translateY(${vRow.start}px)`,
|
|
294
|
+
}}
|
|
295
|
+
className={cn(
|
|
296
|
+
'flex items-center gap-2 border-b border-zinc-100 px-4 text-xs dark:border-zinc-900',
|
|
297
|
+
isHighlighted && 'bg-zinc-100 dark:bg-zinc-800',
|
|
298
|
+
)}
|
|
299
|
+
onMouseEnter={() => setSearchHighlightIndex(vRow.index)}
|
|
300
|
+
onClick={(e) => {
|
|
301
|
+
if (e.shiftKey) toggleAdditive(r);
|
|
302
|
+
else commit(r, vRow.index);
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
<input
|
|
306
|
+
type="checkbox"
|
|
307
|
+
checked={isChecked}
|
|
308
|
+
onClick={(e) => e.stopPropagation()}
|
|
309
|
+
onChange={() => toggleAdditive(r)}
|
|
310
|
+
aria-label={`Toggle ${r.name || r.globalId} in selection`}
|
|
311
|
+
className="shrink-0 cursor-pointer"
|
|
312
|
+
/>
|
|
313
|
+
<Badge variant="secondary" className="shrink-0 font-mono text-[10px] uppercase">
|
|
314
|
+
{r.typeName}
|
|
315
|
+
</Badge>
|
|
316
|
+
<span className="min-w-0 flex-1 truncate font-medium">
|
|
317
|
+
{r.name || <span className="italic text-muted-foreground">unnamed</span>}
|
|
318
|
+
</span>
|
|
319
|
+
{r.globalId && (
|
|
320
|
+
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
|
|
321
|
+
{r.globalId.slice(0, 10)}…
|
|
322
|
+
</span>
|
|
323
|
+
)}
|
|
324
|
+
{availableModelIds.length > 1 && (
|
|
325
|
+
<span className="shrink-0 rounded border border-zinc-300 px-1 py-0.5 text-[10px] text-muted-foreground dark:border-zinc-700">
|
|
326
|
+
{(models.get(r.modelId)?.name ?? r.modelId).slice(0, 8)}
|
|
327
|
+
</span>
|
|
328
|
+
)}
|
|
329
|
+
<span className="shrink-0 text-[10px] uppercase text-muted-foreground opacity-60">
|
|
330
|
+
{r.matchField}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* ── Footer: counts + batch actions ── */}
|
|
340
|
+
<div className="flex flex-wrap items-center gap-2 border-t px-4 py-2 text-xs">
|
|
341
|
+
<span className="text-muted-foreground">
|
|
342
|
+
{filtered.length} result{filtered.length === 1 ? '' : 's'}
|
|
343
|
+
{filtered.length !== results.length && (
|
|
344
|
+
<span className="ml-1 opacity-70">(of {results.length})</span>
|
|
345
|
+
)}
|
|
346
|
+
{multiCount > 0 && (
|
|
347
|
+
<span className="ml-2 font-medium text-foreground">· {multiCount} selected</span>
|
|
348
|
+
)}
|
|
349
|
+
</span>
|
|
350
|
+
<div className="ml-auto flex items-center gap-1.5">
|
|
351
|
+
<Button
|
|
352
|
+
variant="ghost"
|
|
353
|
+
size="sm"
|
|
354
|
+
onClick={frame}
|
|
355
|
+
disabled={!cameraCallbacks.frameSelection}
|
|
356
|
+
title="Frame primary selection"
|
|
357
|
+
className="h-7 gap-1 text-xs"
|
|
358
|
+
>
|
|
359
|
+
<Crosshair className="h-3 w-3" />
|
|
360
|
+
Frame
|
|
361
|
+
</Button>
|
|
362
|
+
<Button
|
|
363
|
+
variant="ghost"
|
|
364
|
+
size="sm"
|
|
365
|
+
onClick={selectAll}
|
|
366
|
+
disabled={filtered.length === 0}
|
|
367
|
+
title={`Add all ${filtered.length} results to multi-selection`}
|
|
368
|
+
className="h-7 gap-1 text-xs"
|
|
369
|
+
>
|
|
370
|
+
<ListChecks className="h-3 w-3" />
|
|
371
|
+
Select all
|
|
372
|
+
</Button>
|
|
373
|
+
<Button
|
|
374
|
+
variant="ghost"
|
|
375
|
+
size="sm"
|
|
376
|
+
onClick={clearEntitySelection}
|
|
377
|
+
disabled={multiCount === 0}
|
|
378
|
+
title="Clear multi-selection"
|
|
379
|
+
className="h-7 gap-1 text-xs"
|
|
380
|
+
>
|
|
381
|
+
<SquareX className="h-3 w-3" />
|
|
382
|
+
Clear
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
* SearchModal — advanced search (⌘⇧F / Ctrl+Shift+F).
|
|
7
|
+
*
|
|
8
|
+
* Shares `searchSlice.searchQuery` with the inline field, so the modal
|
|
9
|
+
* can never "lose" what you've already typed — open it and the query is
|
|
10
|
+
* already there, adjust it and closing the modal leaves the inline in
|
|
11
|
+
* sync. The tab switcher has a "Search" tab (P3) and a "SQL" tab stub
|
|
12
|
+
* reserved for P4. All search engines (Tier-0 linear scan, Tier-1 token
|
|
13
|
+
* index) are reused — the modal just renders a bigger, unfiltered,
|
|
14
|
+
* virtualized version of what the inline popover shows.
|
|
15
|
+
*
|
|
16
|
+
* Keyboard (inside the modal):
|
|
17
|
+
* • ↑ / ↓ — navigate result rows
|
|
18
|
+
* • Enter — commit (select + frame + enter vim cycle + close)
|
|
19
|
+
* • ⇧Enter — toggle row in multi-selection (stays open)
|
|
20
|
+
* • Esc — close modal
|
|
21
|
+
* • ⌘⇧F / Ctrl+⇧F — toggle modal closed (symmetric with open)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
25
|
+
import { Search, SlidersHorizontal } from 'lucide-react';
|
|
26
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
27
|
+
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
|
28
|
+
import { Input } from '@/components/ui/input';
|
|
29
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
30
|
+
import { useViewerStore } from '@/store';
|
|
31
|
+
import { runTier0Scan, type SearchResult, type ScanModel } from '@/lib/search/tier0-scan';
|
|
32
|
+
import { queryTier1Indexes, type Tier1Index } from '@/lib/search/tier1-index';
|
|
33
|
+
import { useSearchIndex } from '@/hooks/useSearchIndex';
|
|
34
|
+
import { pushRecentSearch } from '@/lib/search/recent-searches';
|
|
35
|
+
import { SearchModalText } from './SearchModal.text';
|
|
36
|
+
import { SearchModalFilter } from './SearchModal.filter';
|
|
37
|
+
|
|
38
|
+
/** Modal-side result cap. Well above what any user scrolls through, small
|
|
39
|
+
* enough that the score/merge arrays stay cheap. Virtualization keeps
|
|
40
|
+
* DOM cost constant regardless. */
|
|
41
|
+
const RESULT_LIMIT_MODAL = 5000;
|
|
42
|
+
const DEBOUNCE_MS = 80;
|
|
43
|
+
|
|
44
|
+
export function SearchModal() {
|
|
45
|
+
const {
|
|
46
|
+
searchQuery,
|
|
47
|
+
searchModalOpen,
|
|
48
|
+
searchIndexes,
|
|
49
|
+
models,
|
|
50
|
+
setSearchModalOpen,
|
|
51
|
+
toggleSearchModal,
|
|
52
|
+
setSearchQuery,
|
|
53
|
+
} = useViewerStore(
|
|
54
|
+
useShallow((s) => ({
|
|
55
|
+
searchQuery: s.searchQuery,
|
|
56
|
+
searchModalOpen: s.searchModalOpen,
|
|
57
|
+
searchIndexes: s.searchIndexes,
|
|
58
|
+
models: s.models,
|
|
59
|
+
setSearchModalOpen: s.setSearchModalOpen,
|
|
60
|
+
toggleSearchModal: s.toggleSearchModal,
|
|
61
|
+
setSearchQuery: s.setSearchQuery,
|
|
62
|
+
})),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Make sure Tier-1 indexes continue building while the modal is open
|
|
66
|
+
// (the inline also mounts this hook — cheap re-registration).
|
|
67
|
+
useSearchIndex();
|
|
68
|
+
|
|
69
|
+
// Debounce the query the same way the inline does, so fast typing
|
|
70
|
+
// inside the modal doesn't re-scan per keystroke.
|
|
71
|
+
const [debouncedQuery, setDebouncedQuery] = useState(searchQuery);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const handle = window.setTimeout(() => setDebouncedQuery(searchQuery), DEBOUNCE_MS);
|
|
74
|
+
return () => window.clearTimeout(handle);
|
|
75
|
+
}, [searchQuery]);
|
|
76
|
+
|
|
77
|
+
// Split models into the two search tiers. Same logic as SearchInline.
|
|
78
|
+
const { tier0Models, tier1Indexes, availableModelIds } = useMemo(() => {
|
|
79
|
+
const t0: ScanModel[] = [];
|
|
80
|
+
const t1: Tier1Index[] = [];
|
|
81
|
+
const ids: string[] = [];
|
|
82
|
+
for (const m of models.values()) {
|
|
83
|
+
if (!m.ifcDataStore) continue;
|
|
84
|
+
ids.push(m.id);
|
|
85
|
+
const record = searchIndexes.get(m.id);
|
|
86
|
+
if (record?.status === 'ready' && record.index) {
|
|
87
|
+
t1.push(record.index);
|
|
88
|
+
} else {
|
|
89
|
+
t0.push({ id: m.id, ifcDataStore: m.ifcDataStore });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { tier0Models: t0, tier1Indexes: t1, availableModelIds: ids };
|
|
93
|
+
}, [models, searchIndexes]);
|
|
94
|
+
|
|
95
|
+
// Full result pool (pre-filter). Filtering happens inside the tab.
|
|
96
|
+
const results = useMemo<SearchResult[]>(() => {
|
|
97
|
+
if (!debouncedQuery.trim()) return [];
|
|
98
|
+
if (tier0Models.length === 0 && tier1Indexes.length === 0) return [];
|
|
99
|
+
|
|
100
|
+
const t1Results = tier1Indexes.length > 0
|
|
101
|
+
? queryTier1Indexes(tier1Indexes, debouncedQuery, { limit: RESULT_LIMIT_MODAL })
|
|
102
|
+
: [];
|
|
103
|
+
const t0Results = tier0Models.length > 0
|
|
104
|
+
? runTier0Scan(tier0Models, debouncedQuery, { limit: RESULT_LIMIT_MODAL })
|
|
105
|
+
: [];
|
|
106
|
+
|
|
107
|
+
if (t1Results.length === 0) return t0Results;
|
|
108
|
+
if (t0Results.length === 0) return t1Results;
|
|
109
|
+
|
|
110
|
+
const combined = [...t1Results, ...t0Results];
|
|
111
|
+
combined.sort((a, b) => {
|
|
112
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
113
|
+
if (a.modelId !== b.modelId) return a.modelId < b.modelId ? -1 : 1;
|
|
114
|
+
return a.expressId - b.expressId;
|
|
115
|
+
});
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
const out: SearchResult[] = [];
|
|
118
|
+
for (const r of combined) {
|
|
119
|
+
const key = `${r.modelId}:${r.expressId}`;
|
|
120
|
+
if (seen.has(key)) continue;
|
|
121
|
+
seen.add(key);
|
|
122
|
+
out.push(r);
|
|
123
|
+
if (out.length >= RESULT_LIMIT_MODAL) break;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}, [tier0Models, tier1Indexes, debouncedQuery]);
|
|
127
|
+
|
|
128
|
+
/** Global ⌘⇧F / Ctrl+⇧F toggle — opens from anywhere, also closes when open. */
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const handler = (e: globalThis.KeyboardEvent) => {
|
|
131
|
+
const isAdvancedShortcut =
|
|
132
|
+
(e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'f' || e.key === 'F');
|
|
133
|
+
if (isAdvancedShortcut) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
toggleSearchModal();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
window.addEventListener('keydown', handler);
|
|
139
|
+
return () => window.removeEventListener('keydown', handler);
|
|
140
|
+
}, [toggleSearchModal]);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Record the query in recents on the modal-close *transition* — once
|
|
144
|
+
* per close, with the final query at that moment. We watch only
|
|
145
|
+
* `searchModalOpen` (not `searchQuery`) so typing in the inline bar
|
|
146
|
+
* while the modal is closed never fires this effect; without that
|
|
147
|
+
* gate, every keystroke in the inline bar (which shares `searchQuery`
|
|
148
|
+
* with the modal) would push a partial-prefix recent.
|
|
149
|
+
*
|
|
150
|
+
* `prevOpenRef` distinguishes the "opened then closed" transition
|
|
151
|
+
* from the initial mount where `searchModalOpen` is already false.
|
|
152
|
+
*/
|
|
153
|
+
const prevOpenRef = useRef(searchModalOpen);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const wasOpen = prevOpenRef.current;
|
|
156
|
+
prevOpenRef.current = searchModalOpen;
|
|
157
|
+
if (wasOpen && !searchModalOpen) {
|
|
158
|
+
// Use the latest searchQuery via a fresh read — depending on it
|
|
159
|
+
// would re-fire this effect on every keystroke. Since the close
|
|
160
|
+
// transition is what we care about, the latest value at close
|
|
161
|
+
// time is the right thing to record.
|
|
162
|
+
const q = searchQuery.trim();
|
|
163
|
+
if (q) pushRecentSearch(q);
|
|
164
|
+
}
|
|
165
|
+
}, [searchModalOpen, searchQuery]);
|
|
166
|
+
|
|
167
|
+
// Auto-select the input on open so typing is immediate.
|
|
168
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (searchModalOpen) {
|
|
171
|
+
// Next tick so Radix Dialog has mounted the content.
|
|
172
|
+
const t = window.setTimeout(() => {
|
|
173
|
+
inputRef.current?.focus();
|
|
174
|
+
inputRef.current?.select();
|
|
175
|
+
}, 10);
|
|
176
|
+
return () => window.clearTimeout(t);
|
|
177
|
+
}
|
|
178
|
+
}, [searchModalOpen]);
|
|
179
|
+
|
|
180
|
+
const close = useCallback(() => setSearchModalOpen(false), [setSearchModalOpen]);
|
|
181
|
+
|
|
182
|
+
if (!searchModalOpen) return null;
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Dialog open={searchModalOpen} onOpenChange={(open) => setSearchModalOpen(open)}>
|
|
186
|
+
<DialogContent
|
|
187
|
+
hideCloseButton
|
|
188
|
+
className="max-w-4xl h-[80vh] p-0 gap-0 flex flex-col"
|
|
189
|
+
onEscapeKeyDown={close}
|
|
190
|
+
>
|
|
191
|
+
<DialogTitle className="sr-only">Advanced Search</DialogTitle>
|
|
192
|
+
<Tabs defaultValue="search" className="flex flex-col flex-1 min-h-0">
|
|
193
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
194
|
+
<TabsList>
|
|
195
|
+
<TabsTrigger value="search">
|
|
196
|
+
<Search className="h-3.5 w-3.5 mr-1.5" />
|
|
197
|
+
Search
|
|
198
|
+
</TabsTrigger>
|
|
199
|
+
<TabsTrigger value="filter">
|
|
200
|
+
<SlidersHorizontal className="h-3.5 w-3.5 mr-1.5" />
|
|
201
|
+
Filter
|
|
202
|
+
</TabsTrigger>
|
|
203
|
+
</TabsList>
|
|
204
|
+
<div className="text-[11px] text-muted-foreground">
|
|
205
|
+
<kbd className="rounded border border-zinc-300 bg-zinc-100 px-1 font-mono text-[10px] dark:border-zinc-700 dark:bg-zinc-900">Esc</kbd>
|
|
206
|
+
<span className="ml-1">close</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<TabsContent value="search" className="flex-1 min-h-0 mt-0 flex flex-col">
|
|
210
|
+
<div className="border-b px-4 py-3">
|
|
211
|
+
<Input
|
|
212
|
+
ref={inputRef}
|
|
213
|
+
type="text"
|
|
214
|
+
placeholder="Search GUID, name, type, description, objectType…"
|
|
215
|
+
value={searchQuery}
|
|
216
|
+
leftIcon={<Search className="h-4 w-4" />}
|
|
217
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
218
|
+
className="h-10 text-sm"
|
|
219
|
+
aria-label="Advanced search query"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<SearchModalText
|
|
223
|
+
results={results}
|
|
224
|
+
availableModelIds={availableModelIds}
|
|
225
|
+
onClose={close}
|
|
226
|
+
/>
|
|
227
|
+
</TabsContent>
|
|
228
|
+
<TabsContent value="filter" className="flex-1 min-h-0 mt-0 flex">
|
|
229
|
+
<SearchModalFilter />
|
|
230
|
+
</TabsContent>
|
|
231
|
+
</Tabs>
|
|
232
|
+
</DialogContent>
|
|
233
|
+
</Dialog>
|
|
234
|
+
);
|
|
235
|
+
}
|