@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
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
ArrowDown,
|
|
30
30
|
Zap,
|
|
31
31
|
} from 'lucide-react';
|
|
32
|
-
import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/clerk-react';
|
|
33
32
|
import { Button } from '@/components/ui/button';
|
|
34
33
|
import { toast } from '@/components/ui/toast';
|
|
35
34
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -38,6 +37,7 @@ import { buildErrorFeedbackContent } from '@/store/slices/chatSlice';
|
|
|
38
37
|
import { ChatMessageComponent } from './chat/ChatMessage';
|
|
39
38
|
import { ModelSelector } from './chat/ModelSelector';
|
|
40
39
|
import { fetchUsageSnapshot, streamChat, type StreamMessage, type TextContentPart, type ImageContentPart, type UsageInfo } from '@/lib/llm/stream-client';
|
|
40
|
+
import { streamAnthropicChat, streamOpenAiChat } from '@/lib/llm/stream-direct';
|
|
41
41
|
import { buildStreamMessagesForModel, filterAttachmentsForModel } from '@/lib/llm/message-capabilities';
|
|
42
42
|
import { buildSystemPrompt } from '@/lib/llm/system-prompt';
|
|
43
43
|
import { getModelContext, parseCSV } from '@/lib/llm/context-builder';
|
|
@@ -49,11 +49,11 @@ import type { ScriptDiagnostic } from '@/lib/llm/script-diagnostics';
|
|
|
49
49
|
import { buildRepairSessionKey, getEscalatedRepairScope, pruneMessagesForRepair } from '@/lib/llm/repair-loop';
|
|
50
50
|
import type { ChatMessage, ChatRepairRequest, FileAttachment } from '@/lib/llm/types';
|
|
51
51
|
import { canUsePlainCodeBlockFallback, type ScriptMutationIntent } from '@/lib/llm/script-preservation';
|
|
52
|
-
import { Image as ImageIcon } from 'lucide-react';
|
|
53
|
-
import {
|
|
54
|
-
import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess } from '@/lib/desktop-product';
|
|
55
|
-
import { navigateToPath } from '@/services/app-navigation';
|
|
52
|
+
import { Check, Image as ImageIcon, Key, Eye, EyeOff, ExternalLink } from 'lucide-react';
|
|
53
|
+
import { hasDesktopFeatureAccess } from '@/lib/desktop-product';
|
|
56
54
|
import { getModelById } from '@/lib/llm/models';
|
|
55
|
+
import { resolveStreamRoute } from '@/lib/llm/byok-guard';
|
|
56
|
+
import { getApiKeys, updateApiKeys, hasAnthropicKey, hasOpenaiKey, subscribeApiKeys } from '@/services/api-keys';
|
|
57
57
|
import { useSandbox } from '@/hooks/useSandbox';
|
|
58
58
|
|
|
59
59
|
// Environment variable for the proxy URL
|
|
@@ -67,7 +67,6 @@ const EXAMPLE_PROMPTS = [
|
|
|
67
67
|
];
|
|
68
68
|
|
|
69
69
|
const CONTINUE_PROMPT = 'Continue from exactly where your last response stopped. Do not repeat previously generated text.';
|
|
70
|
-
const DEFAULT_PRO_MONTHLY_CREDIT_LIMIT = 1000;
|
|
71
70
|
const USAGE_REFRESH_INTERVAL_MS = 15_000;
|
|
72
71
|
const EST_CHARS_PER_TOKEN = 4;
|
|
73
72
|
const IMAGE_TOKEN_COST_EST = 850;
|
|
@@ -80,11 +79,24 @@ const MAX_INLINE_IMAGE_DATA_URL_CHARS = 1_200_000;
|
|
|
80
79
|
const MAX_ATTACHMENTS_PER_MESSAGE = 6;
|
|
81
80
|
const MAX_TEXT_ATTACHMENT_BYTES = 512_000;
|
|
82
81
|
const MAX_IMAGE_ATTACHMENT_BYTES = 8_000_000;
|
|
82
|
+
/** Anthropic's PDF content-block limit is ~32 MB; keep our upload cap lower. */
|
|
83
|
+
const MAX_PDF_ATTACHMENT_BYTES = 16_000_000;
|
|
83
84
|
|
|
84
85
|
function createAttachmentId(): string {
|
|
85
86
|
return crypto.randomUUID();
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/** Convert an ArrayBuffer (binary file) to raw base64 — no data-URL prefix. */
|
|
90
|
+
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
91
|
+
const bytes = new Uint8Array(buffer);
|
|
92
|
+
let binary = '';
|
|
93
|
+
const chunk = 0x8000;
|
|
94
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
95
|
+
binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)));
|
|
96
|
+
}
|
|
97
|
+
return btoa(binary);
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
interface ChatSendOptions {
|
|
89
101
|
continuationBase?: string;
|
|
90
102
|
intent?: ScriptMutationIntent;
|
|
@@ -211,23 +223,30 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
211
223
|
const consumePendingPrompt = useViewerStore((s) => s.consumeChatPendingPrompt);
|
|
212
224
|
const pendingRepairRequest = useViewerStore((s) => s.chatPendingRepairRequest);
|
|
213
225
|
const consumePendingRepairRequest = useViewerStore((s) => s.consumeChatPendingRepairRequest);
|
|
214
|
-
const
|
|
215
|
-
const
|
|
226
|
+
const hasByokKey = useViewerStore((s) => s.chatHasByokKey);
|
|
227
|
+
const setChatHasByokKey = useViewerStore((s) => s.setChatHasByokKey);
|
|
216
228
|
const usage = useViewerStore((s) => s.chatUsage);
|
|
217
229
|
const setChatUsage = useViewerStore((s) => s.setChatUsage);
|
|
218
230
|
const desktopEntitlement = useViewerStore((s) => s.desktopEntitlement);
|
|
219
231
|
const { execute } = useSandbox();
|
|
220
232
|
const canUseAiAssistant = hasDesktopFeatureAccess(desktopEntitlement, 'ai_assistant');
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
|
|
234
|
+
// Sync BYOK key availability into the store and track per-provider state
|
|
235
|
+
const [keyStateAnthropic, setKeyStateAnthropic] = useState(hasAnthropicKey);
|
|
236
|
+
const [keyStateOpenai, setKeyStateOpenai] = useState(hasOpenaiKey);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const refresh = () => {
|
|
239
|
+
const a = hasAnthropicKey();
|
|
240
|
+
const o = hasOpenaiKey();
|
|
241
|
+
setKeyStateAnthropic(a);
|
|
242
|
+
setKeyStateOpenai(o);
|
|
243
|
+
setChatHasByokKey(a || o);
|
|
244
|
+
};
|
|
245
|
+
refresh();
|
|
246
|
+
return subscribeApiKeys(refresh);
|
|
247
|
+
}, [setChatHasByokKey]);
|
|
248
|
+
|
|
249
|
+
const displayUsage: UsageInfo | null = usage;
|
|
231
250
|
const usageResetLabel = displayUsage?.resetAt && displayUsage.resetAt > 0
|
|
232
251
|
? new Date(displayUsage.resetAt * 1000).toLocaleDateString()
|
|
233
252
|
: '—';
|
|
@@ -238,14 +257,10 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
238
257
|
const [showScrollBtn, setShowScrollBtn] = useState(false);
|
|
239
258
|
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
|
240
259
|
const [lastFinishReason, setLastFinishReason] = useState<string | null>(null);
|
|
241
|
-
const openUpgradePage = useCallback(() => {
|
|
242
|
-
navigateToPath(buildDesktopUpgradeUrl());
|
|
243
|
-
}, []);
|
|
244
260
|
const promptAiUpgrade = useCallback(() => {
|
|
245
261
|
setChatError('AI assistant is available with Desktop Pro.');
|
|
246
262
|
toast.info('AI assistant is available with Desktop Pro');
|
|
247
|
-
|
|
248
|
-
}, [openUpgradePage, setChatError]);
|
|
263
|
+
}, [setChatError]);
|
|
249
264
|
|
|
250
265
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
251
266
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -307,7 +322,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
307
322
|
useEffect(() => {
|
|
308
323
|
let cancelled = false;
|
|
309
324
|
const refreshUsage = async () => {
|
|
310
|
-
const snapshot = await fetchUsageSnapshot(PROXY_URL
|
|
325
|
+
const snapshot = await fetchUsageSnapshot(PROXY_URL);
|
|
311
326
|
if (!cancelled && snapshot) {
|
|
312
327
|
setChatUsage(snapshot);
|
|
313
328
|
}
|
|
@@ -322,7 +337,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
322
337
|
cancelled = true;
|
|
323
338
|
window.clearInterval(timer);
|
|
324
339
|
};
|
|
325
|
-
}, [
|
|
340
|
+
}, [setChatUsage]);
|
|
326
341
|
|
|
327
342
|
// ── Keyboard shortcuts ──
|
|
328
343
|
useEffect(() => {
|
|
@@ -403,6 +418,20 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
403
418
|
return;
|
|
404
419
|
}
|
|
405
420
|
|
|
421
|
+
// Resolve the stream route BEFORE any user-visible side effects (adding
|
|
422
|
+
// the user message, clearing attachments, setting sending state). If the
|
|
423
|
+
// selected BYOK model has no key, bail out now so the chat transcript
|
|
424
|
+
// doesn't stack orphaned user messages on repeated sends.
|
|
425
|
+
const route = resolveStreamRoute(activeModel, getApiKeys());
|
|
426
|
+
if (route.kind === 'missing-key') {
|
|
427
|
+
setChatError(
|
|
428
|
+
route.provider === 'anthropic'
|
|
429
|
+
? 'Enter your Anthropic API key above to use this model.'
|
|
430
|
+
: 'Enter your OpenAI API key above to use this model.',
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
406
435
|
const continuationBase = options?.continuationBase;
|
|
407
436
|
const responseIntent = options?.intent ?? 'create';
|
|
408
437
|
if (responseIntent !== 'repair') {
|
|
@@ -588,14 +617,8 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
588
617
|
}
|
|
589
618
|
};
|
|
590
619
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
model: activeModel,
|
|
594
|
-
messages: streamMessages,
|
|
595
|
-
system: systemPrompt,
|
|
596
|
-
authToken,
|
|
597
|
-
signal: abortController.signal,
|
|
598
|
-
onChunk: (chunk) => {
|
|
620
|
+
// ── Shared stream callbacks ──
|
|
621
|
+
const handleChunk = (chunk: string) => {
|
|
599
622
|
clearPendingAttachmentsOnce();
|
|
600
623
|
accumulated += chunk;
|
|
601
624
|
if (!responseEditState.applyFailed && responseEditState.intent !== 'repair') {
|
|
@@ -629,8 +652,8 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
629
652
|
}
|
|
630
653
|
setChatStatus('streaming');
|
|
631
654
|
updateStreaming(accumulated);
|
|
632
|
-
|
|
633
|
-
|
|
655
|
+
};
|
|
656
|
+
const handleComplete = (fullText: string) => {
|
|
634
657
|
clearPendingAttachmentsOnce();
|
|
635
658
|
const normalizedText = continuationBase
|
|
636
659
|
? stripContinuationOverlap(continuationBase, fullText)
|
|
@@ -772,22 +795,62 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
772
795
|
}
|
|
773
796
|
|
|
774
797
|
commitAssistantTurn();
|
|
775
|
-
|
|
776
|
-
|
|
798
|
+
};
|
|
799
|
+
const handleUsageInfo = (info: UsageInfo) => {
|
|
777
800
|
setChatUsage(info);
|
|
778
|
-
|
|
779
|
-
|
|
801
|
+
};
|
|
802
|
+
const handleFinishReason = (reason: string | null) => {
|
|
780
803
|
setLastFinishReason(reason);
|
|
781
804
|
if (reason === 'length') {
|
|
782
805
|
setChatError('Response reached output limit. Click Continue to resume.');
|
|
783
806
|
}
|
|
784
|
-
|
|
785
|
-
|
|
807
|
+
};
|
|
808
|
+
const handleError = (err: Error) => {
|
|
786
809
|
setChatError(err.message);
|
|
787
810
|
setChatAbortController(null);
|
|
788
811
|
commitAssistantTurn();
|
|
789
|
-
|
|
790
|
-
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// Route to direct provider streaming for BYOK models, or through the proxy
|
|
815
|
+
// for free models. The route was already resolved (and the missing-key
|
|
816
|
+
// case handled) at the top of doSend, so this dispatch is total.
|
|
817
|
+
if (route.kind === 'anthropic') {
|
|
818
|
+
await streamAnthropicChat(route.apiKey, {
|
|
819
|
+
model: activeModel,
|
|
820
|
+
messages: streamMessages,
|
|
821
|
+
system: systemPrompt,
|
|
822
|
+
signal: abortController.signal,
|
|
823
|
+
onChunk: handleChunk,
|
|
824
|
+
onComplete: handleComplete,
|
|
825
|
+
onFinishReason: handleFinishReason,
|
|
826
|
+
onError: handleError,
|
|
827
|
+
});
|
|
828
|
+
} else if (route.kind === 'openai') {
|
|
829
|
+
await streamOpenAiChat(route.apiKey, {
|
|
830
|
+
model: activeModel,
|
|
831
|
+
messages: streamMessages,
|
|
832
|
+
system: systemPrompt,
|
|
833
|
+
signal: abortController.signal,
|
|
834
|
+
onChunk: handleChunk,
|
|
835
|
+
onComplete: handleComplete,
|
|
836
|
+
onFinishReason: handleFinishReason,
|
|
837
|
+
onError: handleError,
|
|
838
|
+
});
|
|
839
|
+
} else {
|
|
840
|
+
await streamChat({
|
|
841
|
+
proxyUrl: PROXY_URL,
|
|
842
|
+
model: activeModel,
|
|
843
|
+
messages: streamMessages,
|
|
844
|
+
system: systemPrompt,
|
|
845
|
+
signal: abortController.signal,
|
|
846
|
+
onChunk: handleChunk,
|
|
847
|
+
onComplete: handleComplete,
|
|
848
|
+
onFinishReason: handleFinishReason,
|
|
849
|
+
onError: handleError,
|
|
850
|
+
onUsageInfo: handleUsageInfo,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
791
854
|
if (abortController.signal.aborted) {
|
|
792
855
|
commitAssistantTurn();
|
|
793
856
|
const currentState = useViewerStore.getState();
|
|
@@ -797,7 +860,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
797
860
|
}
|
|
798
861
|
}
|
|
799
862
|
}, [
|
|
800
|
-
canUseAiAssistant, status, activeModel, attachments,
|
|
863
|
+
canUseAiAssistant, status, activeModel, attachments,
|
|
801
864
|
addMessage, setChatStatus, updateStreaming, finalizeAssistant,
|
|
802
865
|
setChatError, setChatAbortController, clearAttachments, setChatUsage, resizeInput,
|
|
803
866
|
buildRepairPromptFromLiveState, triggerAutoRepair, execute,
|
|
@@ -969,7 +1032,54 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
969
1032
|
remainingSlots -= 1;
|
|
970
1033
|
continue;
|
|
971
1034
|
}
|
|
972
|
-
//
|
|
1035
|
+
// PDFs are supported by Claude as native document content blocks.
|
|
1036
|
+
// Route them separately from text attachments so the chat request
|
|
1037
|
+
// can emit the correct multimodal block type.
|
|
1038
|
+
if (file.name.match(/\.pdf$/i) || file.type === 'application/pdf') {
|
|
1039
|
+
if (!supportsFileAttachments) {
|
|
1040
|
+
setChatError('Selected model does not support file attachments. Switch model to attach PDFs.');
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (file.size > MAX_PDF_ATTACHMENT_BYTES) {
|
|
1044
|
+
setChatError(`PDF attachments must be smaller than ${Math.round(MAX_PDF_ATTACHMENT_BYTES / 1_000_000)} MB.`);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const buffer = await file.arrayBuffer();
|
|
1048
|
+
const base64 = arrayBufferToBase64(buffer);
|
|
1049
|
+
const attachment: FileAttachment = {
|
|
1050
|
+
id: createAttachmentId(),
|
|
1051
|
+
name: file.name,
|
|
1052
|
+
type: 'application/pdf',
|
|
1053
|
+
size: file.size,
|
|
1054
|
+
pdfBase64: base64,
|
|
1055
|
+
isPdf: true,
|
|
1056
|
+
};
|
|
1057
|
+
addAttachment(attachment);
|
|
1058
|
+
remainingSlots -= 1;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
// Excel / ODS binaries — we can't parse them yet, but we don't want
|
|
1062
|
+
// to silently drop them. Register a metadata-only attachment so the
|
|
1063
|
+
// user (and the LLM via the system prompt) know it's there and can
|
|
1064
|
+
// suggest exporting as CSV.
|
|
1065
|
+
if (file.name.match(/\.(xlsx|xls|ods)$/i)) {
|
|
1066
|
+
if (!supportsFileAttachments) {
|
|
1067
|
+
setChatError('Selected model does not support file attachments. Switch model to attach spreadsheets.');
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const attachment: FileAttachment = {
|
|
1071
|
+
id: createAttachmentId(),
|
|
1072
|
+
name: file.name,
|
|
1073
|
+
type: file.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1074
|
+
size: file.size,
|
|
1075
|
+
isSpreadsheetBinary: true,
|
|
1076
|
+
textContent: `[Binary spreadsheet ${file.name} (${Math.round(file.size / 1024)} KB). Export to CSV for full content access.]`,
|
|
1077
|
+
};
|
|
1078
|
+
addAttachment(attachment);
|
|
1079
|
+
remainingSlots -= 1;
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
// Text-based files — CSV, TSV, JSON, TXT
|
|
973
1083
|
if (!file.name.match(/\.(csv|json|txt|tsv)$/i)) continue;
|
|
974
1084
|
if (!supportsFileAttachments) {
|
|
975
1085
|
setChatError('Selected model does not support file attachments. Switch model to attach files.');
|
|
@@ -1067,12 +1177,17 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1067
1177
|
const modelSupportsImages = modelForUi?.supportsImages ?? false;
|
|
1068
1178
|
const modelSupportsFiles = modelForUi?.supportsFileAttachments ?? true;
|
|
1069
1179
|
const attachmentAccept = [
|
|
1070
|
-
modelSupportsFiles
|
|
1180
|
+
modelSupportsFiles
|
|
1181
|
+
? '.csv,.json,.txt,.tsv,.pdf,application/pdf,.xlsx,.xls,.ods,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/vnd.oasis.opendocument.spreadsheet'
|
|
1182
|
+
: '',
|
|
1071
1183
|
modelSupportsImages ? 'image/*' : '',
|
|
1072
1184
|
].filter(Boolean).join(',');
|
|
1073
1185
|
const canAttachInput = modelSupportsFiles || modelSupportsImages;
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1186
|
+
// Detect when selected model needs a missing BYOK key (reactive state, not raw reads)
|
|
1187
|
+
const modelSource = modelForUi?.source ?? 'proxy';
|
|
1188
|
+
const needsAnthropicKey = modelSource === 'anthropic' && !keyStateAnthropic;
|
|
1189
|
+
const needsOpenaiKey = modelSource === 'openai' && !keyStateOpenai;
|
|
1190
|
+
const needsByokKey = needsAnthropicKey || needsOpenaiKey;
|
|
1076
1191
|
const showSupportEmail = Boolean(error && error.includes('louis@ltplus.com'));
|
|
1077
1192
|
const canContinue = Boolean(
|
|
1078
1193
|
!isActive && (streamingContent.trim().length > 0 || lastFinishReason === 'length'),
|
|
@@ -1111,7 +1226,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1111
1226
|
<TooltipContent>Clear</TooltipContent>
|
|
1112
1227
|
</Tooltip>
|
|
1113
1228
|
|
|
1114
|
-
<ModelSelector
|
|
1229
|
+
<ModelSelector />
|
|
1115
1230
|
<div className="flex-1" />
|
|
1116
1231
|
|
|
1117
1232
|
<Tooltip>
|
|
@@ -1128,31 +1243,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1128
1243
|
<TooltipContent>Auto-run: {autoExecute ? 'ON' : 'OFF'}</TooltipContent>
|
|
1129
1244
|
</Tooltip>
|
|
1130
1245
|
|
|
1131
|
-
{clerkEnabled && (
|
|
1132
|
-
<>
|
|
1133
|
-
<SignedOut>
|
|
1134
|
-
<SignInButton mode="modal">
|
|
1135
|
-
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground">
|
|
1136
|
-
Sign in
|
|
1137
|
-
</Button>
|
|
1138
|
-
</SignInButton>
|
|
1139
|
-
</SignedOut>
|
|
1140
|
-
{!hasPro && (
|
|
1141
|
-
<Button
|
|
1142
|
-
variant="ghost"
|
|
1143
|
-
size="sm"
|
|
1144
|
-
className="h-6 px-2 text-xs text-muted-foreground"
|
|
1145
|
-
onClick={openUpgradePage}
|
|
1146
|
-
>
|
|
1147
|
-
Pro
|
|
1148
|
-
</Button>
|
|
1149
|
-
)}
|
|
1150
|
-
<SignedIn>
|
|
1151
|
-
<UserButton afterSignOutUrl="/" />
|
|
1152
|
-
</SignedIn>
|
|
1153
|
-
</>
|
|
1154
|
-
)}
|
|
1155
|
-
|
|
1156
1246
|
{onClose && (
|
|
1157
1247
|
<Button variant="ghost" size="icon-xs" onClick={onClose}>
|
|
1158
1248
|
<X className="h-3.5 w-3.5" />
|
|
@@ -1161,14 +1251,16 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1161
1251
|
</div>
|
|
1162
1252
|
|
|
1163
1253
|
{!canUseAiAssistant && (
|
|
1164
|
-
<div className="border-b bg-muted/40 px-3 py-2 text-xs text-muted-foreground
|
|
1165
|
-
|
|
1166
|
-
<Button variant="outline" size="sm" className="h-6 px-2 text-xs" onClick={openUpgradePage}>
|
|
1167
|
-
Upgrade
|
|
1168
|
-
</Button>
|
|
1254
|
+
<div className="border-b bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
|
1255
|
+
AI assistant requires Desktop Pro. Core viewing and scripting stay available without it.
|
|
1169
1256
|
</div>
|
|
1170
1257
|
)}
|
|
1171
1258
|
|
|
1259
|
+
{/* Inline BYOK key prompt — shown when user picks a model without the matching key */}
|
|
1260
|
+
{needsByokKey && canUseAiAssistant && (
|
|
1261
|
+
<InlineKeyPrompt provider={needsAnthropicKey ? 'anthropic' : 'openai'} />
|
|
1262
|
+
)}
|
|
1263
|
+
|
|
1172
1264
|
{/* Clear confirmation */}
|
|
1173
1265
|
{showClearConfirm && (
|
|
1174
1266
|
<div className="px-3 py-2 bg-destructive/5 border-b flex items-center gap-2 text-xs">
|
|
@@ -1273,16 +1365,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1273
1365
|
Continue
|
|
1274
1366
|
</Button>
|
|
1275
1367
|
)}
|
|
1276
|
-
{showUpgradeNudge && clerkEnabled && (
|
|
1277
|
-
<Button
|
|
1278
|
-
variant="outline"
|
|
1279
|
-
size="sm"
|
|
1280
|
-
className="h-5 px-2 text-[10px]"
|
|
1281
|
-
onClick={openUpgradePage}
|
|
1282
|
-
>
|
|
1283
|
-
Upgrade
|
|
1284
|
-
</Button>
|
|
1285
|
-
)}
|
|
1286
1368
|
{showSupportEmail && (
|
|
1287
1369
|
<a className="underline text-[10px]" href="mailto:louis@ltplus.com">
|
|
1288
1370
|
Contact support
|
|
@@ -1351,7 +1433,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1351
1433
|
</TooltipTrigger>
|
|
1352
1434
|
<TooltipContent>
|
|
1353
1435
|
{!canUseAiAssistant
|
|
1354
|
-
? '
|
|
1436
|
+
? 'AI assistant not available'
|
|
1355
1437
|
: canAttachInput
|
|
1356
1438
|
? 'Attach file or image (paste, drag & drop)'
|
|
1357
1439
|
: 'Selected model does not support attachments'}
|
|
@@ -1367,11 +1449,11 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1367
1449
|
}}
|
|
1368
1450
|
onKeyDown={handleKeyDown}
|
|
1369
1451
|
onPaste={handlePaste}
|
|
1370
|
-
placeholder={canUseAiAssistant ? '
|
|
1452
|
+
placeholder={!canUseAiAssistant ? 'AI assistant not available' : needsByokKey ? `Enter your ${needsAnthropicKey ? 'Anthropic' : 'OpenAI'} API key above` : 'Ask anything...'}
|
|
1371
1453
|
rows={1}
|
|
1372
1454
|
className="flex-1 resize-none rounded-md border border-input bg-background text-foreground placeholder:text-muted-foreground px-3 py-1.5 text-sm min-h-[32px] max-h-[120px] focus:outline-none focus:ring-1 focus:ring-ring"
|
|
1373
1455
|
style={{ height: 'auto', overflow: 'hidden' }}
|
|
1374
|
-
disabled={!canUseAiAssistant}
|
|
1456
|
+
disabled={!canUseAiAssistant || needsByokKey}
|
|
1375
1457
|
/>
|
|
1376
1458
|
|
|
1377
1459
|
{isActive ? (
|
|
@@ -1395,7 +1477,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1395
1477
|
variant="default"
|
|
1396
1478
|
size="icon-xs"
|
|
1397
1479
|
onClick={handleSend}
|
|
1398
|
-
disabled={!inputText.trim() || !canUseAiAssistant}
|
|
1480
|
+
disabled={!inputText.trim() || !canUseAiAssistant || needsByokKey}
|
|
1399
1481
|
className="shrink-0 mb-0.5"
|
|
1400
1482
|
>
|
|
1401
1483
|
<Send className="h-3.5 w-3.5" />
|
|
@@ -1438,3 +1520,87 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1438
1520
|
</div>
|
|
1439
1521
|
);
|
|
1440
1522
|
}
|
|
1523
|
+
|
|
1524
|
+
// ── Inline BYOK key prompt (shown inside chat panel when key is missing) ──
|
|
1525
|
+
|
|
1526
|
+
const PROVIDER_INFO = {
|
|
1527
|
+
anthropic: {
|
|
1528
|
+
label: 'Anthropic',
|
|
1529
|
+
placeholder: 'sk-ant-api03-...',
|
|
1530
|
+
url: 'https://console.anthropic.com/settings/keys',
|
|
1531
|
+
urlLabel: 'console.anthropic.com',
|
|
1532
|
+
},
|
|
1533
|
+
openai: {
|
|
1534
|
+
label: 'OpenAI',
|
|
1535
|
+
placeholder: 'sk-...',
|
|
1536
|
+
url: 'https://platform.openai.com/api-keys',
|
|
1537
|
+
urlLabel: 'platform.openai.com',
|
|
1538
|
+
},
|
|
1539
|
+
} as const;
|
|
1540
|
+
|
|
1541
|
+
function InlineKeyPrompt({ provider }: { provider: 'anthropic' | 'openai' }) {
|
|
1542
|
+
const [value, setValue] = useState('');
|
|
1543
|
+
const [show, setShow] = useState(false);
|
|
1544
|
+
const [saved, setSaved] = useState(false);
|
|
1545
|
+
const info = PROVIDER_INFO[provider];
|
|
1546
|
+
|
|
1547
|
+
const handleSave = useCallback(() => {
|
|
1548
|
+
const trimmed = value.trim();
|
|
1549
|
+
if (!trimmed) return;
|
|
1550
|
+
if (provider === 'anthropic') {
|
|
1551
|
+
updateApiKeys({ anthropicKey: trimmed });
|
|
1552
|
+
} else {
|
|
1553
|
+
updateApiKeys({ openaiKey: trimmed });
|
|
1554
|
+
}
|
|
1555
|
+
setSaved(true);
|
|
1556
|
+
}, [value, provider]);
|
|
1557
|
+
|
|
1558
|
+
// Brief success state before the parent unmounts this component
|
|
1559
|
+
if (saved) {
|
|
1560
|
+
return (
|
|
1561
|
+
<div className="border-b bg-emerald-500/10 px-3 py-2 flex items-center gap-2 text-xs text-emerald-700 dark:text-emerald-400">
|
|
1562
|
+
<Check className="h-3.5 w-3.5" />
|
|
1563
|
+
<span>{info.label} key saved — ready to chat</span>
|
|
1564
|
+
</div>
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return (
|
|
1569
|
+
<div className="border-b bg-muted/30 px-3 py-2.5 space-y-1.5">
|
|
1570
|
+
<div className="flex items-center gap-1.5 text-xs font-medium">
|
|
1571
|
+
<Key className="h-3.5 w-3.5" />
|
|
1572
|
+
{info.label} API key required
|
|
1573
|
+
</div>
|
|
1574
|
+
<p className="text-[11px] text-muted-foreground">
|
|
1575
|
+
Paste your key below — stored in your browser only, sent directly to {info.label}.{' '}
|
|
1576
|
+
<a href={info.url} target="_blank" rel="noopener noreferrer" className="underline inline-flex items-center gap-0.5">
|
|
1577
|
+
Get a key <ExternalLink className="h-2.5 w-2.5" />
|
|
1578
|
+
</a>
|
|
1579
|
+
</p>
|
|
1580
|
+
<div className="flex gap-1.5">
|
|
1581
|
+
<div className="relative flex-1">
|
|
1582
|
+
<input
|
|
1583
|
+
type={show ? 'text' : 'password'}
|
|
1584
|
+
value={value}
|
|
1585
|
+
onChange={(e) => setValue(e.target.value)}
|
|
1586
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }}
|
|
1587
|
+
placeholder={info.placeholder}
|
|
1588
|
+
autoComplete="off"
|
|
1589
|
+
spellCheck={false}
|
|
1590
|
+
className="w-full rounded border border-input bg-background px-2 py-1 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-ring pr-7"
|
|
1591
|
+
/>
|
|
1592
|
+
<button
|
|
1593
|
+
type="button"
|
|
1594
|
+
onClick={() => setShow(!show)}
|
|
1595
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
1596
|
+
>
|
|
1597
|
+
{show ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
1598
|
+
</button>
|
|
1599
|
+
</div>
|
|
1600
|
+
<Button size="sm" className="h-7 px-3 text-xs" onClick={handleSave} disabled={!value.trim()}>
|
|
1601
|
+
Save
|
|
1602
|
+
</Button>
|
|
1603
|
+
</div>
|
|
1604
|
+
</div>
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
@@ -53,6 +53,11 @@ import {
|
|
|
53
53
|
FolderOpen,
|
|
54
54
|
Clock,
|
|
55
55
|
Save,
|
|
56
|
+
CalendarClock,
|
|
57
|
+
CalendarPlus,
|
|
58
|
+
Sparkles,
|
|
59
|
+
Eraser,
|
|
60
|
+
MapPin,
|
|
56
61
|
} from 'lucide-react';
|
|
57
62
|
import { cn } from '@/lib/utils';
|
|
58
63
|
import { useViewerStore } from '@/store';
|
|
@@ -207,23 +212,28 @@ function activateRightPanel(panel: 'bcf' | 'ids' | 'lens') {
|
|
|
207
212
|
// If was active → all closed → falls back to Properties
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
/** Exclusively activate a bottom panel (Script / List).
|
|
215
|
+
/** Exclusively activate a bottom panel (Script / List / Gantt).
|
|
211
216
|
* Closes the other first so the if-else chain in ViewerLayout renders it.
|
|
212
217
|
* If the target is already active, closes it. */
|
|
213
|
-
function activateBottomPanel(panel: 'script' | 'list') {
|
|
218
|
+
function activateBottomPanel(panel: 'script' | 'list' | 'gantt') {
|
|
214
219
|
const s = useViewerStore.getState();
|
|
215
|
-
const isActive =
|
|
220
|
+
const isActive =
|
|
221
|
+
panel === 'script' ? s.scriptPanelVisible
|
|
222
|
+
: panel === 'list' ? s.listPanelVisible
|
|
223
|
+
: s.ganttPanelVisible;
|
|
216
224
|
|
|
217
225
|
closeActiveAnalysisExtension();
|
|
218
226
|
|
|
219
|
-
// Close all bottom panels
|
|
227
|
+
// Close all bottom panels — only one slots into the bottom strip at a time.
|
|
220
228
|
s.setScriptPanelVisible(false);
|
|
221
229
|
s.setListPanelVisible(false);
|
|
230
|
+
s.setGanttPanelVisible(false);
|
|
222
231
|
|
|
223
232
|
if (!isActive) {
|
|
224
233
|
s.setRightPanelCollapsed(false);
|
|
225
234
|
if (panel === 'script') s.setScriptPanelVisible(true);
|
|
226
|
-
else s.setListPanelVisible(true);
|
|
235
|
+
else if (panel === 'list') s.setListPanelVisible(true);
|
|
236
|
+
else s.setGanttPanelVisible(true);
|
|
227
237
|
}
|
|
228
238
|
}
|
|
229
239
|
|
|
@@ -322,6 +332,10 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
322
332
|
action: () => { useViewerStore.getState().setActiveTool('measure'); } },
|
|
323
333
|
{ id: 'tool:section', label: 'Section', keywords: 'clip cut plane', category: 'Tools', icon: Scissors, shortcut: 'X',
|
|
324
334
|
action: () => { useViewerStore.getState().setActiveTool('section'); } },
|
|
335
|
+
{ id: 'tool:annotate', label: 'Annotate', keywords: 'pin note comment marker', category: 'Tools', icon: MapPin, shortcut: 'P',
|
|
336
|
+
action: () => { useViewerStore.getState().setActiveTool('annotate'); } },
|
|
337
|
+
{ id: 'tool:add-element', label: 'Add Element', keywords: 'wall slab beam column place drop new add element generic', category: 'Tools', icon: Box,
|
|
338
|
+
action: () => { useViewerStore.getState().setActiveTool('addElement'); } },
|
|
325
339
|
);
|
|
326
340
|
|
|
327
341
|
// ── Visibility ──
|
|
@@ -362,7 +376,7 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
362
376
|
|
|
363
377
|
// ── Panels ──
|
|
364
378
|
c.push(
|
|
365
|
-
{ id: 'panel:properties', label: '
|
|
379
|
+
{ id: 'panel:properties', label: 'Inspector', keywords: 'properties attributes material classification schedule task panel right', category: 'Panels', icon: Layout,
|
|
366
380
|
action: () => { const s = useViewerStore.getState(); s.setRightPanelCollapsed(!s.rightPanelCollapsed); } },
|
|
367
381
|
{ id: 'panel:tree', label: 'Spatial Tree', keywords: 'hierarchy left panel', category: 'Panels', icon: TreeDeciduous,
|
|
368
382
|
action: () => { const s = useViewerStore.getState(); s.setLeftPanelCollapsed(!s.leftPanelCollapsed); } },
|
|
@@ -372,12 +386,47 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
372
386
|
action: () => { activateRightPanel('bcf'); } },
|
|
373
387
|
{ id: 'panel:ids', label: 'IDS Validation', keywords: 'information delivery specification check', category: 'Panels', icon: ClipboardCheck,
|
|
374
388
|
action: () => { activateRightPanel('ids'); } },
|
|
375
|
-
{ id: 'panel:lists', label: 'Entity Lists', keywords: 'table spreadsheet
|
|
389
|
+
{ id: 'panel:lists', label: 'Entity Lists', keywords: 'table spreadsheet', category: 'Panels', icon: FileSpreadsheet,
|
|
376
390
|
action: () => { activateBottomPanel('list'); } },
|
|
391
|
+
{ id: 'panel:gantt', label: 'Construction Schedule (Gantt)', keywords: '4d timeline tasks ifctask sequence playback animation', category: 'Panels', icon: CalendarClock,
|
|
392
|
+
action: () => { activateBottomPanel('gantt'); } },
|
|
377
393
|
{ id: 'panel:lens', label: 'Lens Rules', keywords: 'color filter highlight', category: 'Panels', icon: Palette,
|
|
378
394
|
action: () => { activateRightPanel('lens'); } },
|
|
379
395
|
);
|
|
380
396
|
|
|
397
|
+
// ── Schedule / 4D (Tools) ─────────────────────────────
|
|
398
|
+
c.push(
|
|
399
|
+
{ id: 'schedule:generate', label: 'Generate Schedule from Storeys…',
|
|
400
|
+
keywords: '4d ifctask construction sequence storey building create gantt',
|
|
401
|
+
category: 'Tools', icon: CalendarPlus,
|
|
402
|
+
action: () => {
|
|
403
|
+
// Make sure the Gantt panel is mounted so the dialog has a host
|
|
404
|
+
// before flipping the dialog flag. Order matters — closing other
|
|
405
|
+
// panels first prevents the bottom strip from rendering them.
|
|
406
|
+
const s = useViewerStore.getState();
|
|
407
|
+
if (!s.ganttPanelVisible) activateBottomPanel('gantt');
|
|
408
|
+
// Same tick is fine — the dialog is portalled via Radix and doesn't
|
|
409
|
+
// depend on GanttPanel finishing its first render.
|
|
410
|
+
useViewerStore.getState().setGenerateScheduleDialogOpen(true);
|
|
411
|
+
} },
|
|
412
|
+
{ id: 'schedule:toggle-animation', label: 'Toggle 4D Construction Animation',
|
|
413
|
+
keywords: 'play pause schedule task gantt simulation',
|
|
414
|
+
category: 'Visibility', icon: Sparkles,
|
|
415
|
+
action: () => {
|
|
416
|
+
const s = useViewerStore.getState();
|
|
417
|
+
s.setAnimationEnabled(!s.animationEnabled);
|
|
418
|
+
} },
|
|
419
|
+
{ id: 'schedule:reset', label: 'Reset Schedule (Clear 4D Data)',
|
|
420
|
+
keywords: 'remove gantt tasks ifctask delete clear',
|
|
421
|
+
category: 'Tools', icon: Eraser,
|
|
422
|
+
action: () => {
|
|
423
|
+
const s = useViewerStore.getState();
|
|
424
|
+
s.setScheduleData(null);
|
|
425
|
+
s.setAnimationEnabled(false);
|
|
426
|
+
s.pauseSchedule();
|
|
427
|
+
} },
|
|
428
|
+
);
|
|
429
|
+
|
|
381
430
|
// ── Export ──
|
|
382
431
|
c.push(
|
|
383
432
|
{ id: 'export:screenshot', label: 'Screenshot', keywords: 'capture png image viewport', category: 'Export', icon: Camera,
|