@ifc-lite/viewer 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +34 -31
- package/CHANGELOG.md +96 -0
- package/dist/assets/{basketViewActivator-Dn_bHUl2.js → basketViewActivator-CU8_toGq.js} +7 -7
- package/dist/assets/{bcf-B9SFl84i.js → bcf-DXGDhw56.js} +23 -23
- package/dist/assets/{deflate-yMpdCIqk.js → deflate-Bb1_H2Yf.js} +1 -1
- package/dist/assets/{exporters-D-BvrNIg.js → exporters-DZhLN0ux.js} +1861 -1658
- package/dist/assets/geometry-controller.worker-DQOSYqtw.js +7 -0
- package/dist/assets/geometry.worker-B62e03Ao.js +1 -0
- package/dist/assets/{geotiff-D1tvcDCb.js → geotiff-y0ZxbRJd.js} +10 -10
- package/dist/assets/{ids-DZLs0snJ.js → ids-DruUNtfD.js} +4 -4
- package/dist/assets/ifc-lite-Ch2T9pP9.js +7 -0
- package/dist/assets/{ifc-lite_bg-DyHX37GQ.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-BIryVCXQ.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
- package/dist/assets/index-Bws3UAkj.css +1 -0
- package/dist/assets/{index-CXSBhkcJ.js → index-Dr88ZlSY.js} +64100 -47030
- package/dist/assets/{jpeg-DUMcZp24.js → jpeg-B3_loqFe.js} +1 -1
- package/dist/assets/lens-PYsLu_MA.js +1 -0
- package/dist/assets/{lerc-IN4uWojP.js → lerc-nkwS8ZUe.js} +1 -1
- package/dist/assets/{lzw-Cnw0hH-m.js → lzw-D3cW5Wpg.js} +1 -1
- package/dist/assets/{native-bridge-BVf2uzoH.js → native-bridge-BcYJooq8.js} +2 -2
- package/dist/assets/{packbits-BskJCwk0.js → packbits-DDN4xzB5.js} +1 -1
- package/dist/assets/{parser.worker-BdtkkaGf.js → parser.worker-BW1IMUed.js} +3 -3
- package/dist/assets/raw-CoIXstQ-.js +1 -0
- package/dist/assets/{sandbox-VLI_y7cl.js → sandbox-DETNEyQb.js} +498 -470
- package/dist/assets/{server-client-BLcKaWQB.js → server-client-CmzJOeS7.js} +1 -1
- package/dist/assets/{wasm-bridge-BAfZh7YT.js → wasm-bridge-CT7mK9W0.js} +1 -1
- package/dist/assets/{webimage-Db2xzze3.js → webimage-CBjgg4up.js} +1 -1
- package/dist/assets/{workerHelpers--sAYm9yN.js → workerHelpers-IEQDo8r3.js} +1 -1
- package/dist/assets/{zstd-BDToOQyD.js → zstd-C8oQ6qdS.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +11 -9
- package/src/App.tsx +5 -2
- package/src/components/extensions/AuditLogPanel.tsx +259 -0
- package/src/components/extensions/BundlePreview.tsx +102 -0
- package/src/components/extensions/CapabilityReview.tsx +333 -0
- package/src/components/extensions/ExtensionDockHost.tsx +192 -0
- package/src/components/extensions/ExtensionToolbarSlot.tsx +106 -0
- package/src/components/extensions/ExtensionsPanel.tsx +481 -0
- package/src/components/extensions/FlavorDialog.tsx +398 -0
- package/src/components/extensions/FlavorImportPreview.tsx +79 -0
- package/src/components/extensions/FlavorIndicator.tsx +81 -0
- package/src/components/extensions/FlavorListView.tsx +318 -0
- package/src/components/extensions/FlavorMergeDialog.tsx +326 -0
- package/src/components/extensions/HelpHint.tsx +182 -0
- package/src/components/extensions/IdeasPanel.tsx +344 -0
- package/src/components/extensions/PlanCard.tsx +227 -0
- package/src/components/extensions/PrivacyPanel.tsx +312 -0
- package/src/components/extensions/PromoteToolDialog.tsx +313 -0
- package/src/components/extensions/RepairQueuePanel.tsx +222 -0
- package/src/components/extensions/icon-registry.ts +92 -0
- package/src/components/extensions/toast-helpers.ts +49 -0
- package/src/components/extensions/widget/WidgetErrorBoundary.tsx +62 -0
- package/src/components/extensions/widget/WidgetRenderer.tsx +428 -0
- package/src/components/viewer/ChatPanel.tsx +251 -3
- package/src/components/viewer/CommandPalette.tsx +74 -4
- package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
- package/src/components/viewer/EntityContextMenu.tsx +70 -0
- package/src/components/viewer/ExportDialog.tsx +9 -1
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +21 -6
- package/src/components/viewer/LensPanel.tsx +50 -0
- package/src/components/viewer/MainToolbar.tsx +170 -87
- package/src/components/viewer/ScriptPanel.tsx +105 -1
- package/src/components/viewer/Section2DPanel.tsx +58 -2
- package/src/components/viewer/StatusBar.tsx +18 -0
- package/src/components/viewer/ViewerLayout.tsx +53 -4
- package/src/components/viewer/Viewport.tsx +72 -0
- package/src/hooks/useActionLogger.test.ts +161 -0
- package/src/hooks/useActionLogger.ts +141 -0
- package/src/hooks/useForkExtension.ts +51 -0
- package/src/hooks/useIfcFederation.ts +7 -1
- package/src/hooks/useInstalledExtensions.ts +43 -0
- package/src/hooks/usePrivacyDisclosure.ts +48 -0
- package/src/hooks/useRunExtensionTests.ts +67 -0
- package/src/hooks/useSlotContributions.ts +38 -0
- package/src/hooks/useSymbolicAnnotations.test.ts +124 -0
- package/src/hooks/useSymbolicAnnotations.ts +776 -0
- package/src/lib/desktop-product.ts +7 -1
- package/src/lib/lens/adapter.ts +14 -0
- package/src/lib/llm/prompt-cache.ts +77 -0
- package/src/lib/llm/stream-client.ts +20 -2
- package/src/lib/llm/stream-direct.ts +11 -1
- package/src/lib/llm/system-prompt.ts +42 -0
- package/src/lib/safe-mode.ts +30 -0
- package/src/sdk/ExtensionHostProvider.tsx +103 -0
- package/src/services/extensions/flavor-service.ts +183 -0
- package/src/services/extensions/host-commands.ts +112 -0
- package/src/services/extensions/host-installer.ts +289 -0
- package/src/services/extensions/host.ts +514 -0
- package/src/services/extensions/idb-flavor-storage.test.ts +140 -0
- package/src/services/extensions/idb-flavor-storage.ts +241 -0
- package/src/services/extensions/idb-log-storage.test.ts +110 -0
- package/src/services/extensions/idb-log-storage.ts +171 -0
- package/src/services/extensions/idb-storage.ts +228 -0
- package/src/services/extensions/runtime-errors.ts +26 -0
- package/src/services/extensions/sandbox-factory.ts +217 -0
- package/src/store/constants.ts +48 -6
- package/src/store/index.ts +6 -1
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/extensionsSlice.ts +90 -0
- package/src/store/slices/lensSlice.ts +28 -0
- package/src/store/slices/visibilitySlice.test.ts +6 -0
- package/src/store/slices/visibilitySlice.ts +17 -8
- package/src/store/types.ts +2 -0
- package/dist/assets/geometry-controller.worker-Cm5pvyR6.js +0 -7
- package/dist/assets/geometry.worker-ClNvXIrj.js +0 -1
- package/dist/assets/ifc-lite-BDg0iIbj.js +0 -7
- package/dist/assets/index-DS_xJQfP.css +0 -1
- package/dist/assets/lens-CpjUdqpw.js +0 -1
- package/dist/assets/raw-DzTtEZIY.js +0 -1
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
Loader2,
|
|
29
29
|
ArrowDown,
|
|
30
30
|
Zap,
|
|
31
|
+
Wrench,
|
|
31
32
|
} from 'lucide-react';
|
|
33
|
+
import { PromoteToolDialog } from '@/components/extensions/PromoteToolDialog';
|
|
32
34
|
import { Button } from '@/components/ui/button';
|
|
33
35
|
import { toast } from '@/components/ui/toast';
|
|
34
36
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -58,6 +60,12 @@ import { ByokKeyModal } from './chat/ByokKeyModal';
|
|
|
58
60
|
import { ByokStreamingPill } from './chat/ByokStreamingPill';
|
|
59
61
|
import type { BYOKProvider } from '@/lib/llm/clipboard-detect';
|
|
60
62
|
import { useSandbox } from '@/hooks/useSandbox';
|
|
63
|
+
import { useOptionalExtensionHost } from '@/sdk/ExtensionHostProvider';
|
|
64
|
+
import {
|
|
65
|
+
classifyIntent,
|
|
66
|
+
packBundle,
|
|
67
|
+
validateBundleResponse,
|
|
68
|
+
} from '@ifc-lite/extensions';
|
|
61
69
|
|
|
62
70
|
// Environment variable for the proxy URL
|
|
63
71
|
const PROXY_URL = import.meta.env.VITE_LLM_PROXY_URL as string || '/api/chat';
|
|
@@ -203,6 +211,68 @@ interface ChatPanelProps {
|
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
214
|
+
const extensionHost = useOptionalExtensionHost();
|
|
215
|
+
/** Most recent chat classification; surfaced in the status bar as authoring telemetry. */
|
|
216
|
+
const [authoringTelemetry, setAuthoringTelemetry] = useState<{
|
|
217
|
+
intent: 'authoring' | 'fork';
|
|
218
|
+
startedAt: number;
|
|
219
|
+
} | null>(null);
|
|
220
|
+
const setPendingAuthoredBundle = useViewerStore((s) => s.setPendingAuthoredBundle);
|
|
221
|
+
const setExtensionsPanelVisible = useViewerStore((s) => s.setExtensionsPanelVisible);
|
|
222
|
+
const setExtensionsRequestedView = useViewerStore((s) => s.setExtensionsRequestedView);
|
|
223
|
+
const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible);
|
|
224
|
+
const chatToolReady = useViewerStore((s) => s.chatToolReady);
|
|
225
|
+
const setChatToolReady = useViewerStore((s) => s.setChatToolReady);
|
|
226
|
+
/**
|
|
227
|
+
* Local: the inline Promote-to-tool dialog (script path). `source`
|
|
228
|
+
* is snapshotted from the live editor when the dialog opens — the
|
|
229
|
+
* dialog is modal so the script can't change underneath it, and
|
|
230
|
+
* snapshotting avoids re-rendering ChatPanel on every keystroke.
|
|
231
|
+
*/
|
|
232
|
+
const [promoteFromChat, setPromoteFromChat] = useState<{ open: boolean; source: string }>({
|
|
233
|
+
open: false,
|
|
234
|
+
source: '',
|
|
235
|
+
});
|
|
236
|
+
/** One-shot guard for the "use the Ideas panel" authoring hint toast. */
|
|
237
|
+
const authoringHintShownRef = useRef(false);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Try to parse an authoring response as an extension bundle. If it
|
|
241
|
+
* validates, pack it and surface a toast linking to the Extensions
|
|
242
|
+
* panel for the user to review and install. Failures stay silent —
|
|
243
|
+
* the regular chat flow already showed the response.
|
|
244
|
+
*/
|
|
245
|
+
const handleAuthoringResponse = useCallback(
|
|
246
|
+
async (fullText: string): Promise<boolean> => {
|
|
247
|
+
try {
|
|
248
|
+
const result = validateBundleResponse(fullText);
|
|
249
|
+
if (!result.ok || !result.manifest || !result.parsed) return false;
|
|
250
|
+
// Assemble a Bundle from the parsed pieces, pack it, hand it
|
|
251
|
+
// to the Extensions panel.
|
|
252
|
+
const files = new Map<string, { path: string; bytes: Uint8Array; text?: string }>();
|
|
253
|
+
const encoder = new TextEncoder();
|
|
254
|
+
const manifestText = JSON.stringify(result.manifest, null, 2);
|
|
255
|
+
files.set('manifest.json', {
|
|
256
|
+
path: 'manifest.json',
|
|
257
|
+
bytes: encoder.encode(manifestText),
|
|
258
|
+
text: manifestText,
|
|
259
|
+
});
|
|
260
|
+
for (const [path, text] of Object.entries(result.parsed.files)) {
|
|
261
|
+
files.set(path, { path, bytes: encoder.encode(text), text });
|
|
262
|
+
}
|
|
263
|
+
const bytes = packBundle({ manifest: result.manifest, files });
|
|
264
|
+
setPendingAuthoredBundle(bytes);
|
|
265
|
+
// Drive the inline CTA card instead of relying on a toast the
|
|
266
|
+
// user scrolls past — the bundle is one click from installed.
|
|
267
|
+
setChatToolReady({ kind: 'bundle', name: result.manifest.name });
|
|
268
|
+
return true;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.warn('[ChatPanel] authoring response parse failed:', err);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
[setPendingAuthoredBundle, setChatToolReady],
|
|
275
|
+
);
|
|
206
276
|
const messages = useViewerStore((s) => s.chatMessages);
|
|
207
277
|
const status = useViewerStore((s) => s.chatStatus);
|
|
208
278
|
const streamingContent = useViewerStore((s) => s.chatStreamingContent);
|
|
@@ -439,6 +509,54 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
439
509
|
setChatError('AI assistant is available with Desktop Pro.');
|
|
440
510
|
return;
|
|
441
511
|
}
|
|
512
|
+
// Clear any stale post-authoring CTA — this turn re-establishes it
|
|
513
|
+
// on completion if it's another authoring turn.
|
|
514
|
+
setChatToolReady(null);
|
|
515
|
+
|
|
516
|
+
// Classify the prompt for the action log and to nudge the user
|
|
517
|
+
// toward plan-first authoring when appropriate. The classifier is
|
|
518
|
+
// rule-based and content-free metadata only — we record the coarse
|
|
519
|
+
// intent the message looks like, never the prompt text itself.
|
|
520
|
+
const classified = options?.intent === 'repair'
|
|
521
|
+
? { intent: 'one-shot' as const, confidence: 1, reason: 'repair turn' }
|
|
522
|
+
: classifyIntent(text, {
|
|
523
|
+
hasExistingExtension: false,
|
|
524
|
+
hasLoadedModel: useViewerStore.getState().models.size > 0,
|
|
525
|
+
});
|
|
526
|
+
extensionHost?.emitAction('chat.message', {
|
|
527
|
+
intent: classified.intent === 'out-of-scope' ? 'one-shot' : classified.intent,
|
|
528
|
+
});
|
|
529
|
+
// For high-confidence authoring/fork intents, suggest the
|
|
530
|
+
// plan-first path. Non-blocking — we still send through the
|
|
531
|
+
// existing chat pipeline so the user always gets a response.
|
|
532
|
+
if (
|
|
533
|
+
(classified.intent === 'authoring' || classified.intent === 'fork')
|
|
534
|
+
&& classified.confidence >= 0.75
|
|
535
|
+
&& !options?.intent
|
|
536
|
+
) {
|
|
537
|
+
// Show the "use the Ideas panel" hint at most once per chat
|
|
538
|
+
// session — a multi-turn authoring conversation shouldn't
|
|
539
|
+
// re-toast it on every message.
|
|
540
|
+
if (!authoringHintShownRef.current) {
|
|
541
|
+
authoringHintShownRef.current = true;
|
|
542
|
+
toast.info(
|
|
543
|
+
classified.intent === 'fork'
|
|
544
|
+
? 'Heads up: that reads like a fork. Use the Extensions → Ideas panel for diff-based authoring.'
|
|
545
|
+
: 'Heads up: that reads like an authoring request. The Extensions → Ideas panel offers plan-first authoring.',
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
setAuthoringTelemetry({ intent: classified.intent, startedAt: Date.now() });
|
|
549
|
+
} else if (classified.intent !== 'authoring' && classified.intent !== 'fork') {
|
|
550
|
+
setAuthoringTelemetry(null);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Authoring turns write code into the Script Editor — open it so
|
|
554
|
+
// the user watches the tool take shape instead of only seeing
|
|
555
|
+
// chat text. Chat is *part* of the authoring surface, not a
|
|
556
|
+
// detour away from it.
|
|
557
|
+
if (classified.intent === 'authoring' || classified.intent === 'fork') {
|
|
558
|
+
setScriptPanelVisible(true);
|
|
559
|
+
}
|
|
442
560
|
|
|
443
561
|
// Resolve the stream route BEFORE any user-visible side effects (adding
|
|
444
562
|
// the user message, clearing attachments, setting sending state). If the
|
|
@@ -528,6 +646,19 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
528
646
|
const fileAttachments = supportsFileAttachments
|
|
529
647
|
? collectActiveFileAttachments(allMessages, filtered.accepted)
|
|
530
648
|
: [];
|
|
649
|
+
// Personal prompt overlay from the active flavor (RFC §06.4) —
|
|
650
|
+
// durable user preferences appended to the system prompt. Best
|
|
651
|
+
// effort: a missing host / flavor / overlay just omits it.
|
|
652
|
+
let personalOverlay: string | undefined;
|
|
653
|
+
if (extensionHost) {
|
|
654
|
+
try {
|
|
655
|
+
const activeFlavor = await extensionHost.flavors.getActive();
|
|
656
|
+
const content = activeFlavor?.promptOverlay?.content?.trim();
|
|
657
|
+
if (content) personalOverlay = content;
|
|
658
|
+
} catch {
|
|
659
|
+
// Overlay is non-essential — never block a chat turn on it.
|
|
660
|
+
}
|
|
661
|
+
}
|
|
531
662
|
const systemPrompt = buildSystemPrompt(modelContext, fileAttachments, {
|
|
532
663
|
content: liveScriptContext.content,
|
|
533
664
|
revision: liveScriptContext.revision,
|
|
@@ -535,6 +666,13 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
535
666
|
}, {
|
|
536
667
|
userPrompt: text.trim(),
|
|
537
668
|
diagnostics: effectiveDiagnostics,
|
|
669
|
+
// Include the extension-authoring contract when the classifier
|
|
670
|
+
// flagged the turn as authoring/fork — the LLM gets the manifest
|
|
671
|
+
// schema, widget DSL, and capability catalogue in context so it
|
|
672
|
+
// can emit a valid bundle.
|
|
673
|
+
includeAuthoringContract:
|
|
674
|
+
classified.intent === 'authoring' || classified.intent === 'fork',
|
|
675
|
+
personalOverlay,
|
|
538
676
|
});
|
|
539
677
|
const contextWindow = activeModelInfo?.contextWindow ?? 128_000;
|
|
540
678
|
const inputBudget = Math.max(
|
|
@@ -815,6 +953,43 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
815
953
|
}
|
|
816
954
|
}
|
|
817
955
|
|
|
956
|
+
// Authoring loop: when the classifier flagged this turn as
|
|
957
|
+
// 'authoring' or 'fork', the response may contain a bundle in
|
|
958
|
+
// the ifc-extension-* fenced format. If it does, surface the
|
|
959
|
+
// bundle CTA. If it doesn't but code landed in the editor,
|
|
960
|
+
// surface the script CTA so "promote to tool" is one click
|
|
961
|
+
// away — the user never has to hunt for the Promote button.
|
|
962
|
+
//
|
|
963
|
+
// Offer the script-path install CTA whenever the assistant
|
|
964
|
+
// produced runnable code this turn — NOT only on authoring-
|
|
965
|
+
// classified turns. The classifier tags follow-up messages
|
|
966
|
+
// ("yes, use Pset_DoorCommon") as one-shot, but that's often
|
|
967
|
+
// the turn where the final code lands. A one-shot script is
|
|
968
|
+
// just as promotable as an "authored" one.
|
|
969
|
+
const offerScriptInstall = () => {
|
|
970
|
+
if (options?.intent === 'repair') return;
|
|
971
|
+
const wroteCode = responseEditState.appliedAny || responseEditState.fallbackApplied;
|
|
972
|
+
const code = useViewerStore.getState().scriptEditorContent;
|
|
973
|
+
const hasRealCode =
|
|
974
|
+
code.trim().length > 0 && !/Write your BIM script here/.test(code);
|
|
975
|
+
if (wroteCode && hasRealCode) {
|
|
976
|
+
setChatToolReady({ kind: 'script', name: '' });
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
if (
|
|
981
|
+
(classified.intent === 'authoring' || classified.intent === 'fork')
|
|
982
|
+
&& !options?.intent
|
|
983
|
+
) {
|
|
984
|
+
// Authoring-classified turn — try the bundle path first; if
|
|
985
|
+
// no bundle was emitted, fall back to the script CTA.
|
|
986
|
+
void handleAuthoringResponse(fullText).then((bundleFound) => {
|
|
987
|
+
if (!bundleFound) offerScriptInstall();
|
|
988
|
+
});
|
|
989
|
+
} else {
|
|
990
|
+
offerScriptInstall();
|
|
991
|
+
}
|
|
992
|
+
|
|
818
993
|
commitAssistantTurn();
|
|
819
994
|
};
|
|
820
995
|
const handleUsageInfo = (info: UsageInfo) => {
|
|
@@ -884,7 +1059,8 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
884
1059
|
canUseAiAssistant, status, activeModel, attachments,
|
|
885
1060
|
addMessage, setChatStatus, updateStreaming, finalizeAssistant,
|
|
886
1061
|
setChatError, setChatAbortController, clearAttachments, setChatUsage, resizeInput,
|
|
887
|
-
buildRepairPromptFromLiveState, triggerAutoRepair, execute,
|
|
1062
|
+
buildRepairPromptFromLiveState, triggerAutoRepair, execute, extensionHost,
|
|
1063
|
+
setChatToolReady, handleAuthoringResponse, setScriptPanelVisible,
|
|
888
1064
|
]);
|
|
889
1065
|
|
|
890
1066
|
const handleSend = useCallback(() => {
|
|
@@ -994,20 +1170,24 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
994
1170
|
if (messages.length <= 2) {
|
|
995
1171
|
resetScriptEditorForNewChat();
|
|
996
1172
|
clearMessages();
|
|
1173
|
+
setChatToolReady(null);
|
|
1174
|
+
authoringHintShownRef.current = false;
|
|
997
1175
|
setInputText('');
|
|
998
1176
|
setLastFinishReason(null);
|
|
999
1177
|
} else {
|
|
1000
1178
|
setShowClearConfirm(true);
|
|
1001
1179
|
}
|
|
1002
|
-
}, [messages.length, clearMessages, resetScriptEditorForNewChat]);
|
|
1180
|
+
}, [messages.length, clearMessages, resetScriptEditorForNewChat, setChatToolReady]);
|
|
1003
1181
|
|
|
1004
1182
|
const confirmClear = useCallback(() => {
|
|
1005
1183
|
resetScriptEditorForNewChat();
|
|
1006
1184
|
clearMessages();
|
|
1185
|
+
setChatToolReady(null);
|
|
1186
|
+
authoringHintShownRef.current = false;
|
|
1007
1187
|
setInputText('');
|
|
1008
1188
|
setLastFinishReason(null);
|
|
1009
1189
|
setShowClearConfirm(false);
|
|
1010
|
-
}, [clearMessages, resetScriptEditorForNewChat]);
|
|
1190
|
+
}, [clearMessages, resetScriptEditorForNewChat, setChatToolReady]);
|
|
1011
1191
|
|
|
1012
1192
|
// ── File upload (button + drag-drop + paste) ──
|
|
1013
1193
|
const processFiles = useCallback(async (files: FileList | File[]) => {
|
|
@@ -1260,6 +1440,16 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1260
1440
|
|
|
1261
1441
|
<ModelSelector />
|
|
1262
1442
|
<ByokStreamingPill modelId={activeModel} className="ml-1" />
|
|
1443
|
+
{authoringTelemetry && (
|
|
1444
|
+
<span
|
|
1445
|
+
className="ml-1 text-[10px] uppercase tracking-wide font-semibold bg-primary/15 text-primary rounded px-1.5 py-0.5"
|
|
1446
|
+
title={`Authoring contract attached (${authoringTelemetry.intent})`}
|
|
1447
|
+
>
|
|
1448
|
+
{authoringTelemetry.intent === 'fork' ? 'Fork' : 'Authoring'}
|
|
1449
|
+
{' · '}
|
|
1450
|
+
{Math.round((Date.now() - authoringTelemetry.startedAt) / 1000)}s
|
|
1451
|
+
</span>
|
|
1452
|
+
)}
|
|
1263
1453
|
<div className="flex-1" />
|
|
1264
1454
|
|
|
1265
1455
|
<Tooltip>
|
|
@@ -1395,6 +1585,56 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1395
1585
|
<span className="text-xs">Thinking...</span>
|
|
1396
1586
|
</div>
|
|
1397
1587
|
)}
|
|
1588
|
+
|
|
1589
|
+
{/* Post-authoring install CTA — rendered INLINE at the end of
|
|
1590
|
+
the conversation, directly under the generated code, so the
|
|
1591
|
+
"now install it" step is impossible to miss. Highlighted
|
|
1592
|
+
(ring + accent) the moment a workflow is authored. */}
|
|
1593
|
+
{chatToolReady && status === 'idle' && (
|
|
1594
|
+
<div className="mx-3 my-3 rounded-lg border-2 border-primary bg-primary/10 p-3 shadow-sm ring-2 ring-primary/30">
|
|
1595
|
+
<div className="flex items-center gap-2 mb-1">
|
|
1596
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
|
1597
|
+
<Wrench className="h-4 w-4" />
|
|
1598
|
+
</div>
|
|
1599
|
+
<div className="text-sm font-semibold">
|
|
1600
|
+
{chatToolReady.kind === 'bundle'
|
|
1601
|
+
? `"${chatToolReady.name || 'Your extension'}" is ready`
|
|
1602
|
+
: 'Your tool is ready'}
|
|
1603
|
+
</div>
|
|
1604
|
+
</div>
|
|
1605
|
+
<p className="text-xs text-muted-foreground mb-2.5 pl-9">
|
|
1606
|
+
Last step — turn this into a permanent{' '}
|
|
1607
|
+
<span className="font-medium text-foreground">one-click button in your toolbar</span>.
|
|
1608
|
+
</p>
|
|
1609
|
+
<div className="flex items-center gap-2 pl-9">
|
|
1610
|
+
<Button
|
|
1611
|
+
size="sm"
|
|
1612
|
+
onClick={() => {
|
|
1613
|
+
if (chatToolReady.kind === 'bundle') {
|
|
1614
|
+
setExtensionsRequestedView('installed');
|
|
1615
|
+
setExtensionsPanelVisible(true);
|
|
1616
|
+
} else {
|
|
1617
|
+
setPromoteFromChat({
|
|
1618
|
+
open: true,
|
|
1619
|
+
source: useViewerStore.getState().scriptEditorContent,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
setChatToolReady(null);
|
|
1623
|
+
}}
|
|
1624
|
+
>
|
|
1625
|
+
<Wrench className="mr-1 h-3.5 w-3.5" />
|
|
1626
|
+
{chatToolReady.kind === 'bundle' ? 'Review & install' : 'Install as tool'}
|
|
1627
|
+
</Button>
|
|
1628
|
+
<Button
|
|
1629
|
+
size="sm"
|
|
1630
|
+
variant="ghost"
|
|
1631
|
+
onClick={() => setChatToolReady(null)}
|
|
1632
|
+
>
|
|
1633
|
+
Not now
|
|
1634
|
+
</Button>
|
|
1635
|
+
</div>
|
|
1636
|
+
</div>
|
|
1637
|
+
)}
|
|
1398
1638
|
</div>
|
|
1399
1639
|
|
|
1400
1640
|
{/* Scroll to bottom button */}
|
|
@@ -1469,6 +1709,14 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
|
|
|
1469
1709
|
</div>
|
|
1470
1710
|
)}
|
|
1471
1711
|
|
|
1712
|
+
{/* Promote-to-tool dialog, opened from the post-authoring CTA.
|
|
1713
|
+
`source` was snapshotted from the editor at open time. */}
|
|
1714
|
+
<PromoteToolDialog
|
|
1715
|
+
open={promoteFromChat.open}
|
|
1716
|
+
source={promoteFromChat.source}
|
|
1717
|
+
onClose={() => setPromoteFromChat((p) => ({ ...p, open: false }))}
|
|
1718
|
+
/>
|
|
1719
|
+
|
|
1472
1720
|
{/* Input area */}
|
|
1473
1721
|
<div className="shrink-0 border-t p-2">
|
|
1474
1722
|
<div className="flex items-end gap-1.5">
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
ClipboardCheck,
|
|
45
45
|
FileSpreadsheet,
|
|
46
46
|
Palette,
|
|
47
|
+
Puzzle,
|
|
47
48
|
Camera,
|
|
48
49
|
Download,
|
|
49
50
|
FileJson,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
Sparkles,
|
|
59
60
|
Eraser,
|
|
60
61
|
MapPin,
|
|
62
|
+
Pencil,
|
|
61
63
|
PenLine,
|
|
62
64
|
Slice,
|
|
63
65
|
Layers3,
|
|
@@ -76,11 +78,17 @@ import {
|
|
|
76
78
|
executeBasketClear,
|
|
77
79
|
} from '@/store/basket/basketCommands';
|
|
78
80
|
import { useSandbox } from '@/hooks/useSandbox';
|
|
81
|
+
import { useSlotContributions } from '@/hooks/useSlotContributions';
|
|
82
|
+
import { useOptionalExtensionHost } from '@/sdk/ExtensionHostProvider';
|
|
83
|
+
import { resolveExtensionIcon } from '@/components/extensions/icon-registry';
|
|
84
|
+
import type { CommandContribution } from '@ifc-lite/extensions';
|
|
85
|
+
import { toast as paletteToast } from '@/components/ui/toast';
|
|
79
86
|
import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
|
|
80
87
|
import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
|
|
81
88
|
import { getRecentFiles, formatFileSize, getCachedFile } from '@/lib/recent-files';
|
|
82
89
|
import type { RecentFileEntry } from '@/lib/recent-files';
|
|
83
90
|
import { closeActiveAnalysisExtension } from '@/services/analysis-extensions';
|
|
91
|
+
import { describeRunCommandError } from '@/services/extensions/runtime-errors';
|
|
84
92
|
|
|
85
93
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
86
94
|
|
|
@@ -93,7 +101,8 @@ type Category =
|
|
|
93
101
|
| 'Panels'
|
|
94
102
|
| 'Export'
|
|
95
103
|
| 'Automation'
|
|
96
|
-
| 'Preferences'
|
|
104
|
+
| 'Preferences'
|
|
105
|
+
| 'Extensions';
|
|
97
106
|
|
|
98
107
|
interface Command {
|
|
99
108
|
id: string;
|
|
@@ -190,14 +199,16 @@ function downloadBlob(data: BlobPart, name: string, mime: string) {
|
|
|
190
199
|
URL.revokeObjectURL(url);
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
/** Exclusively activate a right-panel content panel (BCF / IDS / Lens).
|
|
202
|
+
/** Exclusively activate a right-panel content panel (BCF / IDS / Lens / Extensions).
|
|
194
203
|
* Closes all others first so the if-else chain in ViewerLayout renders it.
|
|
195
204
|
* If the target is already active, closes it (back to Properties). */
|
|
196
|
-
|
|
205
|
+
|
|
206
|
+
function activateRightPanel(panel: 'bcf' | 'ids' | 'lens' | 'extensions') {
|
|
197
207
|
const s = useViewerStore.getState();
|
|
198
208
|
const isActive =
|
|
199
209
|
panel === 'bcf' ? s.bcfPanelVisible :
|
|
200
210
|
panel === 'ids' ? s.idsPanelVisible :
|
|
211
|
+
panel === 'extensions' ? s.extensionsPanelVisible :
|
|
201
212
|
s.lensPanelVisible;
|
|
202
213
|
|
|
203
214
|
closeActiveAnalysisExtension();
|
|
@@ -206,12 +217,14 @@ function activateRightPanel(panel: 'bcf' | 'ids' | 'lens') {
|
|
|
206
217
|
s.setBcfPanelVisible(false);
|
|
207
218
|
s.setIdsPanelVisible(false);
|
|
208
219
|
s.setLensPanelVisible(false);
|
|
220
|
+
s.setExtensionsPanelVisible(false);
|
|
209
221
|
|
|
210
222
|
if (!isActive) {
|
|
211
223
|
// Open the target, expand right panel
|
|
212
224
|
s.setRightPanelCollapsed(false);
|
|
213
225
|
if (panel === 'bcf') s.setBcfPanelVisible(true);
|
|
214
226
|
else if (panel === 'ids') s.setIdsPanelVisible(true);
|
|
227
|
+
else if (panel === 'extensions') s.setExtensionsPanelVisible(true);
|
|
215
228
|
else s.setLensPanelVisible(true);
|
|
216
229
|
}
|
|
217
230
|
// If was active → all closed → falls back to Properties
|
|
@@ -259,6 +272,8 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
259
272
|
const navigatedByKeyboard = useRef(false);
|
|
260
273
|
|
|
261
274
|
const { execute } = useSandbox();
|
|
275
|
+
const extensionCommands = useSlotContributions<CommandContribution>('commandPalette');
|
|
276
|
+
const extensionHost = useOptionalExtensionHost();
|
|
262
277
|
|
|
263
278
|
useEffect(() => {
|
|
264
279
|
if (open) {
|
|
@@ -395,6 +410,8 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
395
410
|
action: () => { useViewerStore.getState().toggleTypeVisibility('openings'); } },
|
|
396
411
|
{ id: 'vis:site', label: 'Site', keywords: 'IfcSite terrain show hide', category: 'Visibility', icon: Building2,
|
|
397
412
|
action: () => { useViewerStore.getState().toggleTypeVisibility('site'); } },
|
|
413
|
+
{ id: 'vis:ifcAnnotations', label: 'Annotations & Grids', keywords: 'IfcAnnotation IfcGrid IfcGridAxis 2d drawing symbols text dimension grid axis bubble tag show hide', category: 'Visibility', icon: Pencil,
|
|
414
|
+
action: () => { useViewerStore.getState().toggleTypeVisibility('ifcAnnotations'); } },
|
|
398
415
|
{ id: 'vis:reset-colors', label: 'Reset Colors', keywords: 'clear color override', category: 'Visibility', icon: Palette,
|
|
399
416
|
action: () => { execute('bim.viewer.resetColors()\nconsole.log("Colors reset")'); } },
|
|
400
417
|
);
|
|
@@ -417,6 +434,27 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
417
434
|
action: () => { activateBottomPanel('gantt'); } },
|
|
418
435
|
{ id: 'panel:lens', label: 'Lens Rules', keywords: 'color filter highlight', category: 'Panels', icon: Palette,
|
|
419
436
|
action: () => { activateRightPanel('lens'); } },
|
|
437
|
+
{ id: 'panel:extensions', label: 'Extensions', keywords: 'extension plugin install manage iflx', category: 'Panels', icon: Puzzle,
|
|
438
|
+
action: () => { activateRightPanel('extensions'); } },
|
|
439
|
+
// ── Customization entry points — first-class discoverability
|
|
440
|
+
// for new users who don't know extensions/flavors exist. Each
|
|
441
|
+
// routes to the right surface and pre-seeds context where
|
|
442
|
+
// helpful (e.g. open Ideas tab with the empty-plan flow).
|
|
443
|
+
{ id: 'extensions:author', label: 'Author an extension…',
|
|
444
|
+
keywords: 'create new build plan chat ai extension generate',
|
|
445
|
+
category: 'Tools', icon: Sparkles,
|
|
446
|
+
action: () => {
|
|
447
|
+
const s = useViewerStore.getState();
|
|
448
|
+
activateRightPanel('extensions');
|
|
449
|
+
s.setExtensionsRequestedView('ideas');
|
|
450
|
+
s.setIdeasOpenEmptyPlan(true);
|
|
451
|
+
} },
|
|
452
|
+
{ id: 'extensions:flavors', label: 'Manage flavors…',
|
|
453
|
+
keywords: 'flavor profile switch export import merge customization',
|
|
454
|
+
category: 'Panels', icon: Palette,
|
|
455
|
+
action: () => {
|
|
456
|
+
useViewerStore.getState().setFlavorDialogRequested(true);
|
|
457
|
+
} },
|
|
420
458
|
);
|
|
421
459
|
|
|
422
460
|
// ── Schedule / 4D (Tools) ─────────────────────────────
|
|
@@ -503,8 +541,40 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
|
|
503
541
|
action: () => { useViewerStore.getState().toggleHoverTooltips(); } },
|
|
504
542
|
);
|
|
505
543
|
|
|
544
|
+
// ── Extension contributions ──
|
|
545
|
+
// Surfaced under the "Extensions" category. Clicking dispatches
|
|
546
|
+
// through the activation event so the runtime executes the
|
|
547
|
+
// bundle's command handler (or surfaces the failure clearly).
|
|
548
|
+
for (const contribution of extensionCommands) {
|
|
549
|
+
const payload = contribution.payload;
|
|
550
|
+
if (!payload?.id || !payload.title) continue;
|
|
551
|
+
c.push({
|
|
552
|
+
id: `ext:${payload.id}`,
|
|
553
|
+
label: payload.title,
|
|
554
|
+
keywords: `${payload.id} ${payload.paletteCategory ?? ''} extension`,
|
|
555
|
+
category: 'Extensions',
|
|
556
|
+
// `resolveExtensionIcon` is the shared icon registry the
|
|
557
|
+
// picker writes against, so the icon the user chose is the
|
|
558
|
+
// icon shown in the palette.
|
|
559
|
+
icon: resolveExtensionIcon(payload.icon),
|
|
560
|
+
detail: payload.paletteCategory,
|
|
561
|
+
action: () => {
|
|
562
|
+
if (!extensionHost) return;
|
|
563
|
+
// Fire the activation event first so onCommand:<id>-subscribed
|
|
564
|
+
// extensions wake up, then invoke the command handler. The
|
|
565
|
+
// runtime dedupes activations.
|
|
566
|
+
void extensionHost.dispatcher
|
|
567
|
+
.fire(`onCommand:${payload.id}` as `onCommand:${string}`)
|
|
568
|
+
.then(() => extensionHost.runCommand(payload.id))
|
|
569
|
+
.catch((err) => {
|
|
570
|
+
paletteToast.error(describeRunCommandError(payload.id, err));
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
506
576
|
return c;
|
|
507
|
-
}, [execute, recentFiles]);
|
|
577
|
+
}, [execute, recentFiles, extensionCommands, extensionHost]);
|
|
508
578
|
|
|
509
579
|
// ── Search: score, filter, sort ──
|
|
510
580
|
// When searching, results are FLAT sorted by relevance — no category grouping.
|