@ifc-lite/viewer 1.17.4 → 1.17.6
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 +16 -16
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +117 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-86rgogji.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-CcPS9MK5.js} +2274 -2227
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-BFUYA08u.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-Bfms9I4A.js} +35160 -33084
- package/dist/assets/index-_bfZsDCC.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-DUyLCMZS.js} +104 -104
- package/dist/assets/{sandbox-DZiNLNMk.js → sandbox-C8575tul.js} +4340 -4322
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-BuZK7OST.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-JsqEGDV8.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +7 -7
- package/src/App.tsx +16 -2
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +195 -91
- package/src/components/viewer/MainToolbar.tsx +4 -3
- package/src/components/viewer/PropertiesPanel.tsx +16 -2
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ViewerLayout.tsx +1 -0
- package/src/components/viewer/Viewport.tsx +14 -2
- package/src/components/viewer/ViewportContainer.tsx +49 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- 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/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/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 +1 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +484 -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/types.ts +14 -2
- package/src/main.tsx +1 -10
- package/src/services/api-keys.ts +73 -0
- package/src/store/constants.ts +20 -2
- package/src/store/index.ts +12 -5
- 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/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -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/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;
|
|
@@ -211,23 +210,30 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
211
210
|
const consumePendingPrompt = useViewerStore((s) => s.consumeChatPendingPrompt);
|
|
212
211
|
const pendingRepairRequest = useViewerStore((s) => s.chatPendingRepairRequest);
|
|
213
212
|
const consumePendingRepairRequest = useViewerStore((s) => s.consumeChatPendingRepairRequest);
|
|
214
|
-
const
|
|
215
|
-
const
|
|
213
|
+
const hasByokKey = useViewerStore((s) => s.chatHasByokKey);
|
|
214
|
+
const setChatHasByokKey = useViewerStore((s) => s.setChatHasByokKey);
|
|
216
215
|
const usage = useViewerStore((s) => s.chatUsage);
|
|
217
216
|
const setChatUsage = useViewerStore((s) => s.setChatUsage);
|
|
218
217
|
const desktopEntitlement = useViewerStore((s) => s.desktopEntitlement);
|
|
219
218
|
const { execute } = useSandbox();
|
|
220
219
|
const canUseAiAssistant = hasDesktopFeatureAccess(desktopEntitlement, 'ai_assistant');
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
|
|
221
|
+
// Sync BYOK key availability into the store and track per-provider state
|
|
222
|
+
const [keyStateAnthropic, setKeyStateAnthropic] = useState(hasAnthropicKey);
|
|
223
|
+
const [keyStateOpenai, setKeyStateOpenai] = useState(hasOpenaiKey);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const refresh = () => {
|
|
226
|
+
const a = hasAnthropicKey();
|
|
227
|
+
const o = hasOpenaiKey();
|
|
228
|
+
setKeyStateAnthropic(a);
|
|
229
|
+
setKeyStateOpenai(o);
|
|
230
|
+
setChatHasByokKey(a || o);
|
|
231
|
+
};
|
|
232
|
+
refresh();
|
|
233
|
+
return subscribeApiKeys(refresh);
|
|
234
|
+
}, [setChatHasByokKey]);
|
|
235
|
+
|
|
236
|
+
const displayUsage: UsageInfo | null = usage;
|
|
231
237
|
const usageResetLabel = displayUsage?.resetAt && displayUsage.resetAt > 0
|
|
232
238
|
? new Date(displayUsage.resetAt * 1000).toLocaleDateString()
|
|
233
239
|
: '—';
|
|
@@ -238,14 +244,10 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
238
244
|
const [showScrollBtn, setShowScrollBtn] = useState(false);
|
|
239
245
|
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
|
240
246
|
const [lastFinishReason, setLastFinishReason] = useState<string | null>(null);
|
|
241
|
-
const openUpgradePage = useCallback(() => {
|
|
242
|
-
navigateToPath(buildDesktopUpgradeUrl());
|
|
243
|
-
}, []);
|
|
244
247
|
const promptAiUpgrade = useCallback(() => {
|
|
245
248
|
setChatError('AI assistant is available with Desktop Pro.');
|
|
246
249
|
toast.info('AI assistant is available with Desktop Pro');
|
|
247
|
-
|
|
248
|
-
}, [openUpgradePage, setChatError]);
|
|
250
|
+
}, [setChatError]);
|
|
249
251
|
|
|
250
252
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
251
253
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -307,7 +309,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
307
309
|
useEffect(() => {
|
|
308
310
|
let cancelled = false;
|
|
309
311
|
const refreshUsage = async () => {
|
|
310
|
-
const snapshot = await fetchUsageSnapshot(PROXY_URL
|
|
312
|
+
const snapshot = await fetchUsageSnapshot(PROXY_URL);
|
|
311
313
|
if (!cancelled && snapshot) {
|
|
312
314
|
setChatUsage(snapshot);
|
|
313
315
|
}
|
|
@@ -322,7 +324,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
322
324
|
cancelled = true;
|
|
323
325
|
window.clearInterval(timer);
|
|
324
326
|
};
|
|
325
|
-
}, [
|
|
327
|
+
}, [setChatUsage]);
|
|
326
328
|
|
|
327
329
|
// ── Keyboard shortcuts ──
|
|
328
330
|
useEffect(() => {
|
|
@@ -403,6 +405,20 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
403
405
|
return;
|
|
404
406
|
}
|
|
405
407
|
|
|
408
|
+
// Resolve the stream route BEFORE any user-visible side effects (adding
|
|
409
|
+
// the user message, clearing attachments, setting sending state). If the
|
|
410
|
+
// selected BYOK model has no key, bail out now so the chat transcript
|
|
411
|
+
// doesn't stack orphaned user messages on repeated sends.
|
|
412
|
+
const route = resolveStreamRoute(activeModel, getApiKeys());
|
|
413
|
+
if (route.kind === 'missing-key') {
|
|
414
|
+
setChatError(
|
|
415
|
+
route.provider === 'anthropic'
|
|
416
|
+
? 'Enter your Anthropic API key above to use this model.'
|
|
417
|
+
: 'Enter your OpenAI API key above to use this model.',
|
|
418
|
+
);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
406
422
|
const continuationBase = options?.continuationBase;
|
|
407
423
|
const responseIntent = options?.intent ?? 'create';
|
|
408
424
|
if (responseIntent !== 'repair') {
|
|
@@ -588,14 +604,8 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
588
604
|
}
|
|
589
605
|
};
|
|
590
606
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
model: activeModel,
|
|
594
|
-
messages: streamMessages,
|
|
595
|
-
system: systemPrompt,
|
|
596
|
-
authToken,
|
|
597
|
-
signal: abortController.signal,
|
|
598
|
-
onChunk: (chunk) => {
|
|
607
|
+
// ── Shared stream callbacks ──
|
|
608
|
+
const handleChunk = (chunk: string) => {
|
|
599
609
|
clearPendingAttachmentsOnce();
|
|
600
610
|
accumulated += chunk;
|
|
601
611
|
if (!responseEditState.applyFailed && responseEditState.intent !== 'repair') {
|
|
@@ -629,8 +639,8 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
629
639
|
}
|
|
630
640
|
setChatStatus('streaming');
|
|
631
641
|
updateStreaming(accumulated);
|
|
632
|
-
|
|
633
|
-
|
|
642
|
+
};
|
|
643
|
+
const handleComplete = (fullText: string) => {
|
|
634
644
|
clearPendingAttachmentsOnce();
|
|
635
645
|
const normalizedText = continuationBase
|
|
636
646
|
? stripContinuationOverlap(continuationBase, fullText)
|
|
@@ -772,22 +782,62 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
772
782
|
}
|
|
773
783
|
|
|
774
784
|
commitAssistantTurn();
|
|
775
|
-
|
|
776
|
-
|
|
785
|
+
};
|
|
786
|
+
const handleUsageInfo = (info: UsageInfo) => {
|
|
777
787
|
setChatUsage(info);
|
|
778
|
-
|
|
779
|
-
|
|
788
|
+
};
|
|
789
|
+
const handleFinishReason = (reason: string | null) => {
|
|
780
790
|
setLastFinishReason(reason);
|
|
781
791
|
if (reason === 'length') {
|
|
782
792
|
setChatError('Response reached output limit. Click Continue to resume.');
|
|
783
793
|
}
|
|
784
|
-
|
|
785
|
-
|
|
794
|
+
};
|
|
795
|
+
const handleError = (err: Error) => {
|
|
786
796
|
setChatError(err.message);
|
|
787
797
|
setChatAbortController(null);
|
|
788
798
|
commitAssistantTurn();
|
|
789
|
-
|
|
790
|
-
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Route to direct provider streaming for BYOK models, or through the proxy
|
|
802
|
+
// for free models. The route was already resolved (and the missing-key
|
|
803
|
+
// case handled) at the top of doSend, so this dispatch is total.
|
|
804
|
+
if (route.kind === 'anthropic') {
|
|
805
|
+
await streamAnthropicChat(route.apiKey, {
|
|
806
|
+
model: activeModel,
|
|
807
|
+
messages: streamMessages,
|
|
808
|
+
system: systemPrompt,
|
|
809
|
+
signal: abortController.signal,
|
|
810
|
+
onChunk: handleChunk,
|
|
811
|
+
onComplete: handleComplete,
|
|
812
|
+
onFinishReason: handleFinishReason,
|
|
813
|
+
onError: handleError,
|
|
814
|
+
});
|
|
815
|
+
} else if (route.kind === 'openai') {
|
|
816
|
+
await streamOpenAiChat(route.apiKey, {
|
|
817
|
+
model: activeModel,
|
|
818
|
+
messages: streamMessages,
|
|
819
|
+
system: systemPrompt,
|
|
820
|
+
signal: abortController.signal,
|
|
821
|
+
onChunk: handleChunk,
|
|
822
|
+
onComplete: handleComplete,
|
|
823
|
+
onFinishReason: handleFinishReason,
|
|
824
|
+
onError: handleError,
|
|
825
|
+
});
|
|
826
|
+
} else {
|
|
827
|
+
await streamChat({
|
|
828
|
+
proxyUrl: PROXY_URL,
|
|
829
|
+
model: activeModel,
|
|
830
|
+
messages: streamMessages,
|
|
831
|
+
system: systemPrompt,
|
|
832
|
+
signal: abortController.signal,
|
|
833
|
+
onChunk: handleChunk,
|
|
834
|
+
onComplete: handleComplete,
|
|
835
|
+
onFinishReason: handleFinishReason,
|
|
836
|
+
onError: handleError,
|
|
837
|
+
onUsageInfo: handleUsageInfo,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
791
841
|
if (abortController.signal.aborted) {
|
|
792
842
|
commitAssistantTurn();
|
|
793
843
|
const currentState = useViewerStore.getState();
|
|
@@ -797,7 +847,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
797
847
|
}
|
|
798
848
|
}
|
|
799
849
|
}, [
|
|
800
|
-
canUseAiAssistant, status, activeModel, attachments,
|
|
850
|
+
canUseAiAssistant, status, activeModel, attachments,
|
|
801
851
|
addMessage, setChatStatus, updateStreaming, finalizeAssistant,
|
|
802
852
|
setChatError, setChatAbortController, clearAttachments, setChatUsage, resizeInput,
|
|
803
853
|
buildRepairPromptFromLiveState, triggerAutoRepair, execute,
|
|
@@ -1071,8 +1121,11 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1071
1121
|
modelSupportsImages ? 'image/*' : '',
|
|
1072
1122
|
].filter(Boolean).join(',');
|
|
1073
1123
|
const canAttachInput = modelSupportsFiles || modelSupportsImages;
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1124
|
+
// Detect when selected model needs a missing BYOK key (reactive state, not raw reads)
|
|
1125
|
+
const modelSource = modelForUi?.source ?? 'proxy';
|
|
1126
|
+
const needsAnthropicKey = modelSource === 'anthropic' && !keyStateAnthropic;
|
|
1127
|
+
const needsOpenaiKey = modelSource === 'openai' && !keyStateOpenai;
|
|
1128
|
+
const needsByokKey = needsAnthropicKey || needsOpenaiKey;
|
|
1076
1129
|
const showSupportEmail = Boolean(error && error.includes('louis@ltplus.com'));
|
|
1077
1130
|
const canContinue = Boolean(
|
|
1078
1131
|
!isActive && (streamingContent.trim().length > 0 || lastFinishReason === 'length'),
|
|
@@ -1111,7 +1164,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1111
1164
|
<TooltipContent>Clear</TooltipContent>
|
|
1112
1165
|
</Tooltip>
|
|
1113
1166
|
|
|
1114
|
-
<ModelSelector
|
|
1167
|
+
<ModelSelector />
|
|
1115
1168
|
<div className="flex-1" />
|
|
1116
1169
|
|
|
1117
1170
|
<Tooltip>
|
|
@@ -1128,31 +1181,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1128
1181
|
<TooltipContent>Auto-run: {autoExecute ? 'ON' : 'OFF'}</TooltipContent>
|
|
1129
1182
|
</Tooltip>
|
|
1130
1183
|
|
|
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
1184
|
{onClose && (
|
|
1157
1185
|
<Button variant="ghost" size="icon-xs" onClick={onClose}>
|
|
1158
1186
|
<X className="h-3.5 w-3.5" />
|
|
@@ -1161,14 +1189,16 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1161
1189
|
</div>
|
|
1162
1190
|
|
|
1163
1191
|
{!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>
|
|
1192
|
+
<div className="border-b bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
|
1193
|
+
AI assistant requires Desktop Pro. Core viewing and scripting stay available without it.
|
|
1169
1194
|
</div>
|
|
1170
1195
|
)}
|
|
1171
1196
|
|
|
1197
|
+
{/* Inline BYOK key prompt — shown when user picks a model without the matching key */}
|
|
1198
|
+
{needsByokKey && canUseAiAssistant && (
|
|
1199
|
+
<InlineKeyPrompt provider={needsAnthropicKey ? 'anthropic' : 'openai'} />
|
|
1200
|
+
)}
|
|
1201
|
+
|
|
1172
1202
|
{/* Clear confirmation */}
|
|
1173
1203
|
{showClearConfirm && (
|
|
1174
1204
|
<div className="px-3 py-2 bg-destructive/5 border-b flex items-center gap-2 text-xs">
|
|
@@ -1273,16 +1303,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1273
1303
|
Continue
|
|
1274
1304
|
</Button>
|
|
1275
1305
|
)}
|
|
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
1306
|
{showSupportEmail && (
|
|
1287
1307
|
<a className="underline text-[10px]" href="mailto:louis@ltplus.com">
|
|
1288
1308
|
Contact support
|
|
@@ -1351,7 +1371,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1351
1371
|
</TooltipTrigger>
|
|
1352
1372
|
<TooltipContent>
|
|
1353
1373
|
{!canUseAiAssistant
|
|
1354
|
-
? '
|
|
1374
|
+
? 'AI assistant not available'
|
|
1355
1375
|
: canAttachInput
|
|
1356
1376
|
? 'Attach file or image (paste, drag & drop)'
|
|
1357
1377
|
: 'Selected model does not support attachments'}
|
|
@@ -1367,11 +1387,11 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1367
1387
|
}}
|
|
1368
1388
|
onKeyDown={handleKeyDown}
|
|
1369
1389
|
onPaste={handlePaste}
|
|
1370
|
-
placeholder={canUseAiAssistant ? '
|
|
1390
|
+
placeholder={!canUseAiAssistant ? 'AI assistant not available' : needsByokKey ? `Enter your ${needsAnthropicKey ? 'Anthropic' : 'OpenAI'} API key above` : 'Ask anything...'}
|
|
1371
1391
|
rows={1}
|
|
1372
1392
|
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
1393
|
style={{ height: 'auto', overflow: 'hidden' }}
|
|
1374
|
-
disabled={!canUseAiAssistant}
|
|
1394
|
+
disabled={!canUseAiAssistant || needsByokKey}
|
|
1375
1395
|
/>
|
|
1376
1396
|
|
|
1377
1397
|
{isActive ? (
|
|
@@ -1395,7 +1415,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1395
1415
|
variant="default"
|
|
1396
1416
|
size="icon-xs"
|
|
1397
1417
|
onClick={handleSend}
|
|
1398
|
-
disabled={!inputText.trim() || !canUseAiAssistant}
|
|
1418
|
+
disabled={!inputText.trim() || !canUseAiAssistant || needsByokKey}
|
|
1399
1419
|
className="shrink-0 mb-0.5"
|
|
1400
1420
|
>
|
|
1401
1421
|
<Send className="h-3.5 w-3.5" />
|
|
@@ -1438,3 +1458,87 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1438
1458
|
</div>
|
|
1439
1459
|
);
|
|
1440
1460
|
}
|
|
1461
|
+
|
|
1462
|
+
// ── Inline BYOK key prompt (shown inside chat panel when key is missing) ──
|
|
1463
|
+
|
|
1464
|
+
const PROVIDER_INFO = {
|
|
1465
|
+
anthropic: {
|
|
1466
|
+
label: 'Anthropic',
|
|
1467
|
+
placeholder: 'sk-ant-api03-...',
|
|
1468
|
+
url: 'https://console.anthropic.com/settings/keys',
|
|
1469
|
+
urlLabel: 'console.anthropic.com',
|
|
1470
|
+
},
|
|
1471
|
+
openai: {
|
|
1472
|
+
label: 'OpenAI',
|
|
1473
|
+
placeholder: 'sk-...',
|
|
1474
|
+
url: 'https://platform.openai.com/api-keys',
|
|
1475
|
+
urlLabel: 'platform.openai.com',
|
|
1476
|
+
},
|
|
1477
|
+
} as const;
|
|
1478
|
+
|
|
1479
|
+
function InlineKeyPrompt({ provider }: { provider: 'anthropic' | 'openai' }) {
|
|
1480
|
+
const [value, setValue] = useState('');
|
|
1481
|
+
const [show, setShow] = useState(false);
|
|
1482
|
+
const [saved, setSaved] = useState(false);
|
|
1483
|
+
const info = PROVIDER_INFO[provider];
|
|
1484
|
+
|
|
1485
|
+
const handleSave = useCallback(() => {
|
|
1486
|
+
const trimmed = value.trim();
|
|
1487
|
+
if (!trimmed) return;
|
|
1488
|
+
if (provider === 'anthropic') {
|
|
1489
|
+
updateApiKeys({ anthropicKey: trimmed });
|
|
1490
|
+
} else {
|
|
1491
|
+
updateApiKeys({ openaiKey: trimmed });
|
|
1492
|
+
}
|
|
1493
|
+
setSaved(true);
|
|
1494
|
+
}, [value, provider]);
|
|
1495
|
+
|
|
1496
|
+
// Brief success state before the parent unmounts this component
|
|
1497
|
+
if (saved) {
|
|
1498
|
+
return (
|
|
1499
|
+
<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">
|
|
1500
|
+
<Check className="h-3.5 w-3.5" />
|
|
1501
|
+
<span>{info.label} key saved — ready to chat</span>
|
|
1502
|
+
</div>
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return (
|
|
1507
|
+
<div className="border-b bg-muted/30 px-3 py-2.5 space-y-1.5">
|
|
1508
|
+
<div className="flex items-center gap-1.5 text-xs font-medium">
|
|
1509
|
+
<Key className="h-3.5 w-3.5" />
|
|
1510
|
+
{info.label} API key required
|
|
1511
|
+
</div>
|
|
1512
|
+
<p className="text-[11px] text-muted-foreground">
|
|
1513
|
+
Paste your key below — stored in your browser only, sent directly to {info.label}.{' '}
|
|
1514
|
+
<a href={info.url} target="_blank" rel="noopener noreferrer" className="underline inline-flex items-center gap-0.5">
|
|
1515
|
+
Get a key <ExternalLink className="h-2.5 w-2.5" />
|
|
1516
|
+
</a>
|
|
1517
|
+
</p>
|
|
1518
|
+
<div className="flex gap-1.5">
|
|
1519
|
+
<div className="relative flex-1">
|
|
1520
|
+
<input
|
|
1521
|
+
type={show ? 'text' : 'password'}
|
|
1522
|
+
value={value}
|
|
1523
|
+
onChange={(e) => setValue(e.target.value)}
|
|
1524
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }}
|
|
1525
|
+
placeholder={info.placeholder}
|
|
1526
|
+
autoComplete="off"
|
|
1527
|
+
spellCheck={false}
|
|
1528
|
+
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"
|
|
1529
|
+
/>
|
|
1530
|
+
<button
|
|
1531
|
+
type="button"
|
|
1532
|
+
onClick={() => setShow(!show)}
|
|
1533
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
1534
|
+
>
|
|
1535
|
+
{show ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
1536
|
+
</button>
|
|
1537
|
+
</div>
|
|
1538
|
+
<Button size="sm" className="h-7 px-3 text-xs" onClick={handleSave} disabled={!value.trim()}>
|
|
1539
|
+
Save
|
|
1540
|
+
</Button>
|
|
1541
|
+
</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
@@ -306,6 +306,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
306
306
|
const scriptPanelVisible = useViewerStore((state) => state.scriptPanelVisible);
|
|
307
307
|
const setScriptPanelVisible = useViewerStore((state) => state.setScriptPanelVisible);
|
|
308
308
|
// Cesium 3D overlay state
|
|
309
|
+
const cesiumAvailable = useViewerStore((state) => state.cesiumAvailable);
|
|
309
310
|
const cesiumEnabled = useViewerStore((state) => state.cesiumEnabled);
|
|
310
311
|
const toggleCesium = useViewerStore((state) => state.toggleCesium);
|
|
311
312
|
const storeModels = useViewerStore((state) => state.models);
|
|
@@ -1157,8 +1158,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1157
1158
|
</TooltipContent>
|
|
1158
1159
|
</Tooltip>
|
|
1159
1160
|
|
|
1160
|
-
{/* Cesium 3D Context toggle + settings */}
|
|
1161
|
-
{
|
|
1161
|
+
{/* Cesium 3D Context toggle + settings — web only, only when model has georeferencing */}
|
|
1162
|
+
{cesiumAvailable && !desktopShell && (
|
|
1162
1163
|
<div className="flex items-center">
|
|
1163
1164
|
<Tooltip>
|
|
1164
1165
|
<TooltipTrigger asChild>
|
|
@@ -1293,7 +1294,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1293
1294
|
<ThemeSwitch />
|
|
1294
1295
|
</div>
|
|
1295
1296
|
</TooltipTrigger>
|
|
1296
|
-
<TooltipContent>Toggle theme</TooltipContent>
|
|
1297
|
+
<TooltipContent>Toggle theme (Shift+click for secret mode)</TooltipContent>
|
|
1297
1298
|
</Tooltip>
|
|
1298
1299
|
|
|
1299
1300
|
<Tooltip>
|
|
@@ -33,7 +33,7 @@ import { getNativeEntityDetails } from '@/services/desktop-native-metadata';
|
|
|
33
33
|
import { configureMutationView } from '@/utils/configureMutationView';
|
|
34
34
|
import { IfcQuery } from '@ifc-lite/query';
|
|
35
35
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
36
|
-
import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
36
|
+
import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
37
37
|
import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
|
|
38
38
|
import type { EntityRef, FederatedModel } from '@/store/types';
|
|
39
39
|
|
|
@@ -554,6 +554,13 @@ export function PropertiesPanel() {
|
|
|
554
554
|
return info?.hasGeoreference ? info : null;
|
|
555
555
|
}, [model, ifcDataStore]);
|
|
556
556
|
|
|
557
|
+
// Extract IFC length unit scale (e.g. 0.001 for mm, 0.3048 for ft)
|
|
558
|
+
const lengthUnitScale = useMemo(() => {
|
|
559
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
560
|
+
if (!dataStore?.source?.length || !dataStore?.entityIndex) return 1;
|
|
561
|
+
return extractLengthUnitScale(dataStore.source, dataStore.entityIndex);
|
|
562
|
+
}, [model, ifcDataStore]);
|
|
563
|
+
|
|
557
564
|
// Extract type-level properties (e.g., from IfcWallType's HasPropertySets)
|
|
558
565
|
const typeProperties = useMemo(() => {
|
|
559
566
|
if (!selectedEntity) return null;
|
|
@@ -904,7 +911,12 @@ export function PropertiesPanel() {
|
|
|
904
911
|
}
|
|
905
912
|
|
|
906
913
|
if (!selectedEntityId || (!isNativeLazySelection && (!modelQuery || !entityNode))) {
|
|
907
|
-
// Show model metadata when a single
|
|
914
|
+
// Show model metadata when a single model is loaded and nothing selected.
|
|
915
|
+
// Handles both federated models (models.size >= 1) and legacy single-model path (models.size === 0).
|
|
916
|
+
if (models.size === 1) {
|
|
917
|
+
const singleModel = models.values().next().value as FederatedModel;
|
|
918
|
+
return <ModelMetadataPanel model={singleModel} />;
|
|
919
|
+
}
|
|
908
920
|
if (ifcDataStore && models.size === 0 && geometryResult) {
|
|
909
921
|
const legacyModel: FederatedModel = {
|
|
910
922
|
id: '__legacy__',
|
|
@@ -921,6 +933,7 @@ export function PropertiesPanel() {
|
|
|
921
933
|
};
|
|
922
934
|
return <ModelMetadataPanel model={legacyModel} />;
|
|
923
935
|
}
|
|
936
|
+
// Multi-model or no model loaded: show empty state
|
|
924
937
|
return (
|
|
925
938
|
<div className="h-full flex flex-col border-l-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black">
|
|
926
939
|
<div className="p-3 border-b-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
|
|
@@ -1155,6 +1168,7 @@ export function PropertiesPanel() {
|
|
|
1155
1168
|
schemaVersion={activeDataStore?.schemaVersion}
|
|
1156
1169
|
coordinateInfo={(model?.geometryResult ?? geometryResult)?.coordinateInfo}
|
|
1157
1170
|
geometryResult={model?.geometryResult ?? geometryResult}
|
|
1171
|
+
lengthUnitScale={lengthUnitScale}
|
|
1158
1172
|
/>
|
|
1159
1173
|
</CollapsibleContent>
|
|
1160
1174
|
</Collapsible>
|